Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 16 additions & 14 deletions App-Store-Deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,9 @@ The following phases are complete and in the current build:

**What works today:** auth (email + OAuth ×5), feed (infinite scroll, dig, reply, search, link previews), compose (text/image/video, cross-post, repost, scheduled), lists (CRUD, folders, schema editor, watchers), documents (CRUD, folders, search, public reader), public browse, notifications (tray, preferences), profile, follow, organizations, settings, exports.

**Not yet built (remaining phases):** push notifications, inline document image upload, UGC moderation (report/block/mute/terms gate), post as org, offline document sync, realtime, GitHub integration, tag discovery.
**Not yet built (remaining phases):** inline document image upload, post as org, offline document sync, realtime, GitHub integration, tag discovery.

**Shipped since last update (2026-07-05):** Phase 0.5 (Info.plist: arm64, ITSAppUsesNonExemptEncryption), Phase 14 (UGC safety: report message/user, block/unblock user, mute, terms acceptance on register, blocked users in settings), Phase 9 (push notifications: PushService, APNs delegate, register/unregister device token).

---

Expand Down Expand Up @@ -75,17 +77,17 @@ If the endpoints don't exist, this is **blocked on backend** — escalate
immediately, as it is the true critical-path blocker.

iOS work (after backend is confirmed):
- [ ] `Menu` overflow on every message row in `FeedView`, `MessageThreadView`, public views → "Report…" action → `ReportSheet` (reason picker + optional detail) → POST → toast
- [ ] "Report @user" on `UserProfileView`
- [ ] "Block @user" on message overflow and `UserProfileView`; optimistic local filter on feed
- [ ] `BlockedUsersView` reachable from `SettingsView`; unblock action
- [ ] Mute (local-only if no server endpoint)
- [ ] Terms/community-guidelines acceptance checkbox on `RegisterView` (blocks submit until checked); link to a live URL (confirm below in §2)
- [ ] Surface Terms + Guidelines links in `SettingsView` → About
- [ ] New files: `Views/ReportSheet.swift`, `Views/BlockedUsersView.swift`, `Models/Moderation.swift`
- [ ] New `APIClient` methods: `reportMessage`, `reportUser`, `blockUser`, `unblockUser`, `blockedUsers` (and mute variants if supported)
- [x] `Menu` overflow on every message row in `FeedView`, `MessageThreadView` → "Report…" action → `ReportSheet` (reason picker + optional detail) → POST → toast
- [x] "Report @user" on `UserProfileView`
- [x] "Block @user" on message overflow and `UserProfileView`; optimistic local filter on feed
- [x] `BlockedUsersView` reachable from `SettingsView`; unblock action
- [x] Mute (server-backed via `/api/users/{id}/mute` — APIClient methods added)
- [x] Terms/community-guidelines acceptance checkbox on `RegisterView` (blocks submit until checked); links to `/terms`
- [x] Surface Terms + Community Guidelines links in `SettingsView` → About
- [x] New files: `Views/ReportSheet.swift`, `Views/BlockedUsersView.swift`, `Models/Moderation.swift`, `Services/PushService.swift`
- [x] New `APIClient` methods: `reportMessage`, `reportUser`, `blockUser`, `unblockUser`, `blockedUsers`, `muteUser`, `unmuteUser`, `mutedUsers`
- [ ] Unit tests (MockURLSession) for all new API methods; decoding tests for new models
- [ ] `#Preview` for all new views; `.accessibilityLabel` on all new controls
- [x] `#Preview` for all new views; `.accessibilityLabel` on all new controls

#### Phase 0.5 — Info.plist Hygiene `Tiny` ⛔
One-file PR; do this in parallel with Phase 14.
Expand Down Expand Up @@ -403,9 +405,9 @@ Smoke-test checklist on a real device before submitting for App Store review:
Copy this and tick it off just before submitting.

**Feature gates**
- [ ] Phase 14 — UGC safety shipped (report, block, mute, terms gate)
- [ ] Phase 0.5 — Info.plist hygiene done (`ITSAppUsesNonExemptEncryption`, `arm64`, icon)
- [ ] Phase 9 — Push notifications wired (APNs capability, PushService, register/unregister)
- [x] Phase 14 — UGC safety shipped (report, block, mute, terms gate)
- [x] Phase 0.5 — Info.plist hygiene done (`ITSAppUsesNonExemptEncryption`, `arm64`, icon)
- [x] Phase 9 — Push notifications wired (PushService, register/unregister); APNs capability still needs adding in Xcode

**Accounts & credentials**
- [ ] Apple Developer Program membership active; agreements accepted in ASC
Expand Down
37 changes: 32 additions & 5 deletions InterlinedList.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,13 @@

/* Begin PBXBuildFile section */
0C2A6EE423332702FD29DEBA /* Manrope.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 229B4152945DABCD6D88A59A /* Manrope.ttf */; };
1102DD971BF4AC03027058B9 /* BlockedUsersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E32E0E59381D02A8A4E0A247 /* BlockedUsersView.swift */; };
1775C9B4C2CF08267AF3B138 /* WatchersListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5828367E38948582EF1DF2A /* WatchersListView.swift */; };
17CD6CF35900DC95AA284E3C /* PushService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B79D26B822DCA1188DF37E9 /* PushService.swift */; };
23E11F4CE0298A685F13AA21 /* APIClientGapPhasesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3A38EE80BD5D25C7D290EF5 /* APIClientGapPhasesTests.swift */; };
47FBAC21F771B2813EAD8D37 /* Moderation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1147B978D9E72695E4457454 /* Moderation.swift */; };
5703F883D4E6186DB66E5833 /* NotificationPreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA57E5D72AA1429A40660BA /* NotificationPreference.swift */; };
64F5804ECC25725FD1E58E84 /* APIClientModerationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A569D8DB8DFD3CC59072FDB /* APIClientModerationTests.swift */; };
6C10CC420377B0E8AE5C82DB /* GapModelsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93642F79C3049C4A2ECC8AFF /* GapModelsTests.swift */; };
6CED0218B01DF6AA0FF9D3BF /* ListWatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0470C00F1A0E215224120A1 /* ListWatcher.swift */; };
7201B78F48B60376E06DB08E /* ILColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC5914D730B4355F02E89D81 /* ILColor.swift */; };
Expand Down Expand Up @@ -56,6 +60,8 @@
B1C1D1E1F1A1000B /* DocumentSkeletonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1C1D1E1F1A1000C /* DocumentSkeletonView.swift */; };
B1C1D1E1F1A1000D /* ListItemFormView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1C1D1E1F1A1000E /* ListItemFormView.swift */; };
C1D1E1F1A1B10001 /* Organization.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1D1E1F1A1B10002 /* Organization.swift */; };
C478109CF3BB06C22E121904 /* ReportSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7800699C927CE3E133944E62 /* ReportSheet.swift */; };
CB45081F767583424D54523C /* APIClientPushTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09470364B5A0A559FDE3215F /* APIClientPushTests.swift */; };
CE93B45A2285EF8FD995D9FE /* OrganizationsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0041BEAB46F9186FB5BF907 /* OrganizationsView.swift */; };
D0A1D0A1D0A10001 /* OAuthCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A1D0A1D0A10002 /* OAuthCoordinator.swift */; };
D0A1D0A1D0A10003 /* ChangeEmailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A1D0A1D0A10004 /* ChangeEmailView.swift */; };
Expand Down Expand Up @@ -119,13 +125,18 @@
/* End PBXContainerItemProxy section */

/* Begin PBXFileReference section */
09470364B5A0A559FDE3215F /* APIClientPushTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = APIClientPushTests.swift; sourceTree = "<group>"; };
0EF29F5DC946D80A63000209 /* FollowListView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = FollowListView.swift; sourceTree = "<group>"; };
229B4152945DABCD6D88A59A /* Manrope.ttf */ = {isa = PBXFileReference; includeInIndex = 1; path = InterlinedList/Fonts/Manrope.ttf; sourceTree = SOURCE_ROOT; };
1147B978D9E72695E4457454 /* Moderation.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = Moderation.swift; sourceTree = "<group>"; };
229B4152945DABCD6D88A59A /* Manrope.ttf */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = file; path = InterlinedList/Fonts/Manrope.ttf; sourceTree = SOURCE_ROOT; };
2A70E64AE3EB1E11EB9B7F83 /* PublicListDetailView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PublicListDetailView.swift; sourceTree = "<group>"; };
2DD102A0D8ECD7CFC11DBD4A /* SettingsView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
4EE76647702E9C83B941FA2C /* AppDataStoreTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AppDataStoreTests.swift; path = AppDataStoreTests.swift; sourceTree = "<group>"; };
4EE76647702E9C83B941FA2C /* AppDataStoreTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AppDataStoreTests.swift; sourceTree = "<group>"; };
6A569D8DB8DFD3CC59072FDB /* APIClientModerationTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = APIClientModerationTests.swift; sourceTree = "<group>"; };
74B5AC4D76899CEAF620E402 /* PublicBrowse.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PublicBrowse.swift; sourceTree = "<group>"; };
7A01238CA201A950E76845AE /* SpaceGrotesk.ttf */ = {isa = PBXFileReference; includeInIndex = 1; path = InterlinedList/Fonts/SpaceGrotesk.ttf; sourceTree = SOURCE_ROOT; };
7800699C927CE3E133944E62 /* ReportSheet.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ReportSheet.swift; sourceTree = "<group>"; };
7A01238CA201A950E76845AE /* SpaceGrotesk.ttf */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = file; path = InterlinedList/Fonts/SpaceGrotesk.ttf; sourceTree = SOURCE_ROOT; };
8B79D26B822DCA1188DF37E9 /* PushService.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PushService.swift; sourceTree = "<group>"; };
93642F79C3049C4A2ECC8AFF /* GapModelsTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GapModelsTests.swift; sourceTree = "<group>"; };
98DB21A23AF902D811F28C38 /* PublicDocumentsView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PublicDocumentsView.swift; sourceTree = "<group>"; };
A1B1C1D1E1F10000 /* InterlinedList.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = InterlinedList.app; sourceTree = BUILT_PRODUCTS_DIR; };
Expand Down Expand Up @@ -160,7 +171,7 @@
A3C3D3E3F3A30052 /* Document.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Document.swift; sourceTree = "<group>"; };
A3C3D3E3F3A30054 /* DocumentsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DocumentsView.swift; sourceTree = "<group>"; };
A3C3D3E3F3A30056 /* URLSessionProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSessionProtocol.swift; sourceTree = "<group>"; };
A79A316417B99AB22D2FFDC1 /* JetBrainsMono.ttf */ = {isa = PBXFileReference; includeInIndex = 1; path = InterlinedList/Fonts/JetBrainsMono.ttf; sourceTree = SOURCE_ROOT; };
A79A316417B99AB22D2FFDC1 /* JetBrainsMono.ttf */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = file; path = InterlinedList/Fonts/JetBrainsMono.ttf; sourceTree = SOURCE_ROOT; };
B1C1D1E1F1A10002 /* DataCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataCache.swift; sourceTree = "<group>"; };
B1C1D1E1F1A10004 /* AppDataStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDataStore.swift; sourceTree = "<group>"; };
B1C1D1E1F1A10006 /* SkeletonBlock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SkeletonBlock.swift; sourceTree = "<group>"; };
Expand All @@ -178,10 +189,12 @@
D0A1D0A1D0A1000A /* EmailVerificationBanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmailVerificationBanner.swift; sourceTree = "<group>"; };
D0A1D0A1D0A1000C /* LinkedIdentitiesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkedIdentitiesView.swift; sourceTree = "<group>"; };
D0A1D0A1D0A1000E /* OAuthSignInButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OAuthSignInButton.swift; sourceTree = "<group>"; };
E32E0E59381D02A8A4E0A247 /* BlockedUsersView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BlockedUsersView.swift; sourceTree = "<group>"; };
E5828367E38948582EF1DF2A /* WatchersListView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WatchersListView.swift; sourceTree = "<group>"; };
ECA57E5D72AA1429A40660BA /* NotificationPreference.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NotificationPreference.swift; sourceTree = "<group>"; };
F0470C00F1A0E215224120A1 /* ListWatcher.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ListWatcher.swift; sourceTree = "<group>"; };
F379C02B2FCEB9440069B81C /* InterlinedList.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = InterlinedList.xctestplan; sourceTree = "<group>"; };
F3E6216F2FFB4666000D153A /* InterlinedList.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = InterlinedList.entitlements; sourceTree = "<group>"; };
FC5914D730B4355F02E89D81 /* ILColor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = InterlinedList/Services/ILColor.swift; sourceTree = SOURCE_ROOT; };
T1E5T1E5T1E50000 /* InterlinedListTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = InterlinedListTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
T1E5T1E5T1E50002 /* InterlinedListTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InterlinedListTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -249,7 +262,6 @@
229B4152945DABCD6D88A59A /* Manrope.ttf */,
A79A316417B99AB22D2FFDC1 /* JetBrainsMono.ttf */,
);
name = Fonts;
path = Fonts;
sourceTree = "<group>";
};
Expand All @@ -266,6 +278,7 @@
A1B1C1D1E1F1001D /* InterlinedList */ = {
isa = PBXGroup;
children = (
F3E6216F2FFB4666000D153A /* InterlinedList.entitlements */,
A1B1C1D1E1F10002 /* InterlinedListApp.swift */,
A1B1C1D1E1F1001F /* Models */,
A1B1C1D1E1F10020 /* Services */,
Expand Down Expand Up @@ -300,6 +313,7 @@
F0470C00F1A0E215224120A1 /* ListWatcher.swift */,
ECA57E5D72AA1429A40660BA /* NotificationPreference.swift */,
74B5AC4D76899CEAF620E402 /* PublicBrowse.swift */,
1147B978D9E72695E4457454 /* Moderation.swift */,
);
path = Models;
sourceTree = "<group>";
Expand All @@ -315,6 +329,7 @@
B1C1D1E1F1A10004 /* AppDataStore.swift */,
D0A1D0A1D0A10002 /* OAuthCoordinator.swift */,
FC5914D730B4355F02E89D81 /* ILColor.swift */,
8B79D26B822DCA1188DF37E9 /* PushService.swift */,
);
path = Services;
sourceTree = "<group>";
Expand Down Expand Up @@ -356,6 +371,8 @@
E5828367E38948582EF1DF2A /* WatchersListView.swift */,
C0041BEAB46F9186FB5BF907 /* OrganizationsView.swift */,
2DD102A0D8ECD7CFC11DBD4A /* SettingsView.swift */,
7800699C927CE3E133944E62 /* ReportSheet.swift */,
E32E0E59381D02A8A4E0A247 /* BlockedUsersView.swift */,
);
path = Views;
sourceTree = "<group>";
Expand Down Expand Up @@ -400,6 +417,8 @@
T1E5T1E5T1E5P022 /* APIClientOrganizationsTests.swift */,
T1E5T1E5T1E5P024 /* APIClientDeleteAccountTests.swift */,
B3A38EE80BD5D25C7D290EF5 /* APIClientGapPhasesTests.swift */,
6A569D8DB8DFD3CC59072FDB /* APIClientModerationTests.swift */,
09470364B5A0A559FDE3215F /* APIClientPushTests.swift */,
);
path = APIClientTests;
sourceTree = "<group>";
Expand Down Expand Up @@ -585,6 +604,10 @@
CE93B45A2285EF8FD995D9FE /* OrganizationsView.swift in Sources */,
D0F35ACF98DDE854A8437C7D /* SettingsView.swift in Sources */,
7201B78F48B60376E06DB08E /* ILColor.swift in Sources */,
47FBAC21F771B2813EAD8D37 /* Moderation.swift in Sources */,
C478109CF3BB06C22E121904 /* ReportSheet.swift in Sources */,
1102DD971BF4AC03027058B9 /* BlockedUsersView.swift in Sources */,
17CD6CF35900DC95AA284E3C /* PushService.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down Expand Up @@ -633,6 +656,8 @@
23E11F4CE0298A685F13AA21 /* APIClientGapPhasesTests.swift in Sources */,
6C10CC420377B0E8AE5C82DB /* GapModelsTests.swift in Sources */,
E1F7D081DC89245C81E2047C /* AppDataStoreTests.swift in Sources */,
64F5804ECC25725FD1E58E84 /* APIClientModerationTests.swift in Sources */,
CB45081F767583424D54523C /* APIClientPushTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down Expand Up @@ -756,6 +781,7 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = InterlinedList/InterlinedList.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEAD_CODE_STRIPPING = YES;
Expand Down Expand Up @@ -786,6 +812,7 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = InterlinedList/InterlinedList.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEAD_CODE_STRIPPING = YES;
Expand Down
5 changes: 3 additions & 2 deletions InterlinedList/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,11 @@
</dict>
<key>UILaunchScreen</key>
<dict/>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>armv7</string>
<string>network</string>
<string>arm64</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
Expand Down
8 changes: 8 additions & 0 deletions InterlinedList/InterlinedList.entitlements
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>aps-environment</key>
<string>development</string>
</dict>
</plist>
Loading
Loading