Skip to content

Commit

Permalink
first stages of a swiftui list for notifications
Browse files Browse the repository at this point in the history
changes to keep building that will quickly become moot

Starting to implement SwiftUI version of notifications

project update forgotten

Switch out whole view controller when testing grouped notifications.

make old view work again

Bump deployment target to iOS 17

better view model. follow button loads correctly, showing followers list or account works.

mostly kind of working

rename

rename
  • Loading branch information
whattherestimefor committed Jan 22, 2025
1 parent 571d736 commit 7814812
Show file tree
Hide file tree
Showing 27 changed files with 1,386 additions and 103 deletions.
32 changes: 28 additions & 4 deletions Mastodon.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@
2D6DE40026141DF600A63F6A /* SearchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D6DE3FF26141DF600A63F6A /* SearchViewModel.swift */; };
2D76319F25C1521200929FB9 /* StatusSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D76319E25C1521200929FB9 /* StatusSection.swift */; };
2D7631A825C1535600929FB9 /* StatusTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D7631A725C1535600929FB9 /* StatusTableViewCell.swift */; };
2D7867192625B77500211898 /* NotificationItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D7867182625B77500211898 /* NotificationItem.swift */; };
2D7867192625B77500211898 /* NotificationListItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D7867182625B77500211898 /* NotificationListItem.swift */; };
2D82B9FF25E7863200E36F0F /* OnboardingViewControllerAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D82B9FE25E7863200E36F0F /* OnboardingViewControllerAppearance.swift */; };
2D82BA0525E7897700E36F0F /* MastodonResendEmailViewModelNavigationDelegateShim.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D82BA0425E7897700E36F0F /* MastodonResendEmailViewModelNavigationDelegateShim.swift */; };
2D84350525FF858100EECE90 /* UIScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D84350425FF858100EECE90 /* UIScrollView.swift */; };
Expand Down Expand Up @@ -742,7 +742,7 @@
2D6DE3FF26141DF600A63F6A /* SearchViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchViewModel.swift; sourceTree = "<group>"; };
2D76319E25C1521200929FB9 /* StatusSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusSection.swift; sourceTree = "<group>"; };
2D7631A725C1535600929FB9 /* StatusTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusTableViewCell.swift; sourceTree = "<group>"; };
2D7867182625B77500211898 /* NotificationItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationItem.swift; sourceTree = "<group>"; };
2D7867182625B77500211898 /* NotificationListItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationListItem.swift; sourceTree = "<group>"; };
2D82B9FE25E7863200E36F0F /* OnboardingViewControllerAppearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingViewControllerAppearance.swift; sourceTree = "<group>"; };
2D82BA0425E7897700E36F0F /* MastodonResendEmailViewModelNavigationDelegateShim.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonResendEmailViewModelNavigationDelegateShim.swift; sourceTree = "<group>"; };
2D84350425FF858100EECE90 /* UIScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIScrollView.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1271,7 +1271,26 @@
FBD689B42CCBF09F00CE29F3 /* DonationCampaignViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DonationCampaignViewModel.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
FBBEA04F2D3819080000A900 /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = {
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
membershipExceptions = (
InlinePostPreview.swift,
NotificationListViewController.swift,
NotificationRowView.swift,
NotificationRowViewModel.swift,
TimelinePostCell/ActionButtons.swift,
TimelinePostCell/AuthorHeader.swift,
TimelinePostCell/BoostHeader.swift,
TimelinePostCell/MediaGrid.swift,
TimelinePostCell/TimelinePostCell.swift,
);
target = DB427DD125BAA00100D1B89D /* Mastodon */;
};
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */

/* Begin PBXFileSystemSynchronizedRootGroup section */
FBBEA04E2D380FC70000A900 /* In Progress New Layout and Datamodel */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (FBBEA04F2D3819080000A900 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = "In Progress New Layout and Datamodel"; sourceTree = "<group>"; };
FBC4A4F42D2DA424002E654B /* Beta Testing Settings */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = "Beta Testing Settings"; sourceTree = "<group>"; };
/* End PBXFileSystemSynchronizedRootGroup section */

Expand Down Expand Up @@ -2189,6 +2208,7 @@
DB427DD425BAA00100D1B89D /* Mastodon */ = {
isa = PBXGroup;
children = (
FBBEA04E2D380FC70000A900 /* In Progress New Layout and Datamodel */,
DB89BA1025C10FF5008580ED /* Mastodon.entitlements */,
DB427DE325BAA00100D1B89D /* Info.plist */,
2D76319C25C151DE00929FB9 /* Diffable */,
Expand Down Expand Up @@ -2760,7 +2780,7 @@
DB9D6BF725E4F5690051B173 /* NotificationViewController.swift */,
2D607AD726242FC500B70763 /* NotificationViewModel.swift */,
2D35237926256D920031AF25 /* NotificationSection.swift */,
2D7867182625B77500211898 /* NotificationItem.swift */,
2D7867182625B77500211898 /* NotificationListItem.swift */,
);
path = Notification;
sourceTree = "<group>";
Expand Down Expand Up @@ -3857,7 +3877,7 @@
DB0EF72E26FDB24F00347686 /* SidebarListContentView.swift in Sources */,
D85DF96D2C481AF700A01408 /* NotificationPolicyHeaderView.swift in Sources */,
DBBE1B4525F3474B0081417A /* MastodonPickServerAppearance.swift in Sources */,
2D7867192625B77500211898 /* NotificationItem.swift in Sources */,
2D7867192625B77500211898 /* NotificationListItem.swift in Sources */,
DB45FAB625CA5485005A8AC7 /* UIAlertController.swift in Sources */,
DBE0821525CD382600FD6BBD /* MastodonRegisterViewController.swift in Sources */,
DBEFCD74282A130400C0ABEA /* ReportReasonViewModel.swift in Sources */,
Expand Down Expand Up @@ -4602,6 +4622,7 @@
DEVELOPMENT_TEAM = 5Z4GVSS33P;
INFOPLIST_FILE = Mastodon/Info.plist;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
Expand Down Expand Up @@ -4632,6 +4653,7 @@
EXCLUDED_SOURCE_FILE_NAMES = "Mastodon/Resources/Preview\\ Assets.xcassets";
INFOPLIST_FILE = Mastodon/Info.plist;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
Expand Down Expand Up @@ -4817,6 +4839,7 @@
DEVELOPMENT_TEAM = 5Z4GVSS33P;
INFOPLIST_FILE = Mastodon/Info.plist;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
Expand Down Expand Up @@ -5117,6 +5140,7 @@
DEVELOPMENT_TEAM = 5Z4GVSS33P;
INFOPLIST_FILE = Mastodon/Info.plist;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
Expand Down
2 changes: 1 addition & 1 deletion Mastodon/Coordinator/SceneCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ final public class SceneCoordinator {
)
case .moderationWarning:
break
case ._other:
default:
assertionFailure()
break
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Copyright © 2025 Mastodon gGmbH. All rights reserved.
//
// InlinePostPreview.swift
// Design
//
// Created by Sam on 2024-05-08.
//

import SwiftUI
import MastodonSDK

struct InlinePostPreview: View {
let viewModel: Mastodon.Entity.Status.ViewModel

var body: some View {
VStack(alignment: .leading) {
HStack(spacing: 4) {
if viewModel.needsUserAttribution {
RoundedRectangle(cornerRadius: 4)
.frame(width: 16, height: 16)
Text(viewModel.accountDisplayName ?? "")
.bold()
Text(viewModel.accountFullName ?? "")
.foregroundStyle(.secondary)
Spacer(minLength: 0)
} else if viewModel.isPinned {
// This *should* be a Label but it acts funky when this is in a List (i.e. in UserList)
Group {
Image(systemName: "pin.fill")
Text("Pinned")
}
.bold()
.foregroundStyle(.secondary)
.imageScale(.small)
}
}
.lineLimit(1)
.font(.subheadline)
Text(viewModel.content)
.lineLimit(3)
}
.padding(8)
.frame(maxWidth: .infinity)
.overlay {
RoundedRectangle(cornerRadius: 8)
.fill(.clear)
.stroke(.separator)
}
}
}


//#Preview {
// VStack {
// InlinePostPreview(post: SampleData.samplePost)
// InlinePostPreview(post: SampleData.samplePost, needsUserAttribution: false, isPinned: true)
// }
// .padding()
//}
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
// Copyright © 2025 Mastodon gGmbH. All rights reserved.

import SwiftUI
import MastodonCore
import MastodonSDK
import Combine

class NotificationListViewController: UIHostingController<NotificationListView> {

init() {
let viewModel = NotificationListViewModel()
let root = NotificationListView(viewModel: viewModel)
super.init(rootView: root)
viewModel.navigateToScene = { [weak self] scene, transition in
guard let self else { return }
self.sceneCoordinator?.present(scene: scene, from: self, transition: transition)
}
}

required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) not implemented for NotificationListViewController")
}
}

fileprivate enum ListType {
case everything
case mentions

var pickerLabel: String {
switch self {
case .everything:
"EVERYTHING"
case .mentions:
"MENTIONS"
}
}

var feedKind: MastodonFeedKind {
switch self {
case .everything:
return .notificationsAll
case .mentions:
return .notificationsMentionsOnly
}
}
}
extension ListType: Identifiable {
var id: String {
return pickerLabel
}
}

struct NotificationListView: View {
@ObservedObject private var viewModel: NotificationListViewModel

fileprivate init(viewModel: NotificationListViewModel) {
self.viewModel = viewModel
}

var body: some View {
VStack {
HStack {
Spacer()
Picker(selection: $viewModel.displayedNotifications) {
ForEach(
[ListType.everything, .mentions]
) {
Text($0.pickerLabel)
.tag($0)
}
} label: {
}
.pickerStyle(.segmented)
Spacer()
}

List {
ForEach(viewModel.notificationItems) { item in
rowView(item)
.onTapGesture {
didTap(item: item)
}
}
}
.listStyle(.plain)
}

}

@ViewBuilder func rowView(_ notificationListItem: NotificationListItem) -> some View {
switch notificationListItem {
case .bottomLoader, .middleLoader:
Text("loader not yet implemented")
case .filteredNotificationsInfo:
Text("filtered notifications not yet implemented")
case .notification(let feedItemIdentifier):
// TODO: implement unread using Mastodon.Entity.Marker
let viewModel = NotificationRowViewModel.viewModel(feedItemIdentifier: feedItemIdentifier, isUnread: false)
GroupedNotificationRowView(viewModel: viewModel)
}
}

func didTap(item: NotificationListItem) {
switch item {
case .filteredNotificationsInfo:
return
case .notification(let identifier):
if let notificationInfo =
MastodonFeedItemCacheManager.shared.cachedItem(identifier) as? NotificationInfo {
guard let authBox = AuthenticationServiceProvider.shared.currentActiveUser.value, let me = authBox.cachedAccount else { return }
switch (notificationInfo.type, notificationInfo.isGrouped) {
case (.follow, false):
guard let notificationAuthor = notificationInfo.primaryAuthorAccount else { return }
viewModel.navigateToScene?(.profile(.notMe(me: me, displayAccount: notificationAuthor, relationship: MastodonFeedItemCacheManager.shared.currentRelationship(toAccount: notificationAuthor.id))), .show)
case (.follow, true):
viewModel.navigateToScene?(.follower(viewModel: FollowerListViewModel(authenticationBox: authBox, domain: me.domain, userID: me.id)), .show)
default:
break
}
}
default:
return
}
}
}

@MainActor
fileprivate class NotificationListViewModel: ObservableObject {

var navigateToScene: ((SceneCoordinator.Scene, SceneCoordinator.Transition)->())?

@Published var displayedNotifications: ListType = .everything {
didSet {
createNewFeedLoader()
}
}
@Published var notificationItems: [NotificationListItem] = []

private var feedSubscription: AnyCancellable?
private var feedLoader = MastodonFeedLoader(kind: .notificationsAll)

init() {
createNewFeedLoader()
}

private func createNewFeedLoader() {
feedLoader = MastodonFeedLoader(kind: displayedNotifications.feedKind)
feedSubscription = feedLoader.$records
.receive(on: DispatchQueue.main)
.sink { [weak self] records in
// TODO: add middle loader and bottom loader?
let updatedItems = records.compactMap {
NotificationListItem.fromMastodonFeedItemIdentifier($0)
}
// TODO: add the filtered notifications announcement if needed
self?.notificationItems = updatedItems
}
feedLoader.loadMore(newestAnchor: nil, oldestAnchor: nil)
}
}

extension NotificationListItem {
static func fromMastodonFeedItemIdentifier(_ feedItem: MastodonFeedItemIdentifier) -> NotificationListItem? {
switch feedItem {
case .notification, .notificationGroup:
return .notification(feedItem)
case .status:
return nil
}
}
}
Loading

0 comments on commit 7814812

Please sign in to comment.