- Swift 99.9%
- Python 0.1%
scatto.social is on 0.14.2, which rewrote direct messages. Every endpoint the app calls still works, but three things behave differently. A message request now allows exactly one message until it is accepted. The second send answers 403 with its own sentence; the app showed "Sending the message failed.", which invites a retry that cannot work. It now arrives as DirectMessageSendError.requestNotYetAccepted, on both the text and the photo route, carrying the server's wording. That change turned up an older fault. Pixelfed says "Account size limit reached" when an upload runs out of room, and indicatesStorageQuota matched only "storage" or "quota" — so the two obvious words missed the only message the function exists to catch, and every real out-of-room refusal had been arriving as a plain 403. Harmless while nothing else claimed 403; not harmless the moment this commit gave it a meaning. Now also matches "size limit", with the real wording as a test. Conversation paging takes its cursor from the Link header alone. There is no id in the body that works on both server generations: before 0.14.2 the cursor was the conversation row id, since 0.14.2 it is the last message id, and a conversation's own id is now a snowflake larger than any message id. The old fallback therefore asked for everything older than a huge number, got page one back, and did it again on every scroll — the reducer dropped the duplicates, so the list never ended and never grew. Measured on scatto.social: rel="next" carries the last message id, and a final page sends no next link at all. And two message kinds the rewrite can produce, videos and media, were decoding as unknown while the thread gates attachments on isMedia — so such a message drew an empty bubble and dropped the pictures. Both are named now, and the bubble also renders attachments for an unknown kind, so the next word a server invents degrades rather than disappears. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|---|---|---|
| .github | ||
| Assets | ||
| ClientKit | ||
| CoreData | ||
| EnvironmentKit | ||
| Localization | ||
| PixelfedKit | ||
| Resources | ||
| Scatto | ||
| Scatto.xcodeproj | ||
| ScattoCore | ||
| ScattoShare | ||
| ScattoWidget | ||
| Scripts | ||
| ServicesKit | ||
| SharedAssets.xcassets | ||
| UploadKit | ||
| WidgetsKit | ||
| .gitignore | ||
| .swiftlint.yml | ||
| CLAUDE.md | ||
| LICENSE | ||
| NOTICE | ||
| PRIVACY-NOTICE.md | ||
| README.md | ||
| RELEASE_NOTES.md | ||
| sounds_license.md | ||
| Tips.storekit | ||
Scatto
Scatto is a personal fork of Impressia, the SwiftUI iOS client for Pixelfed. It keeps Impressia's photo-first experience and adds three things Impressia doesn't have: first-class video support, real push notifications, and interaction sound effects.
Scatto runs against the maintainer's own Pixelfed instances (pix.hever.de, scatto.social) and is being prepared for an App Store release. There is no plan to open a public issue tracker on this repo.
What Scatto adds over Impressia
Push notifications
Pixelfed speaks Web Push, which iOS apps can't receive directly — so Scatto pairs with a companion relay, scatto-push-relay, that bridges Pixelfed's Web Push to Apple Push Notification service (APNs).
- Real push for likes, comments, follows, and boosts, delivered even when the app is closed.
- Multi-account and multi-instance: each signed-in account gets its own subscription on its own instance, and enabling push registers them all. Signing into a second account no longer silences the first.
- Deep-linking: tapping a notification opens the post or profile it refers to.
- Device-wide badge: the app-icon badge shows the total unread across all accounts, not just whichever was pushed to last.
- Per-subscription control of the icon badge and interaction sounds, so the in-app toggles are honoured even for pushes that land while the app is closed (iOS applies
aps.badge/aps.soundfrom the payload before any app code runs, so the relay has to be told).
Interaction sounds
Scatto plays a short sound for social interactions — a heartbeat when you like (or are liked), a whoosh when you boost (or are boosted), and a typewriter for comments (yours and received). A single Interaction sounds toggle in Settings gates them (default on). Foreground pushes play through the app; backgrounded pushes carry the sound name in the APNs payload.
Video
First-class video support in an app that upstream keeps deliberately photos-only — the original reason for the fork.
Credits
Scatto exists because Impressia exists. All the hard work is upstream:
- Impressia by Marcin Czachurski and contributors — https://github.com/Impressia/Impressia
- Previously released as Vernissage — https://github.com/VernissageApp/Vernissage
Impressia is licensed under the Apache License 2.0. Scatto keeps the same license (see LICENSE) and preserves upstream attribution in NOTICE and in the per-file copyright headers.
Why fork instead of contribute?
Impressia is intentionally photos-only, and its push-free design fits a pure Pixelfed client. Adding video changes the app's positioning, and push notifications pull in a whole companion service — neither is a good fit for upstreaming. Forking lets us keep pulling in upstream fixes (git fetch upstream && git merge upstream/develop) while diverging on our own feature set (video, push, interaction sounds).
Relationship to upstream
upstreamremote: https://github.com/Impressia/Impressiaoriginremote: this repository- Bundle IDs, target names, and directories have been renamed from
Impressia→Scatto— merges from upstream on those files will conflict and need manual resolution.
Build
Requirements: Xcode 16+, iOS 17 SDK, and either a free Apple ID (7-day sideload) or the Apple Developer Program ($99/yr) for stable installs.
git clone git@git.hever.de:vinz/scatto.git
cd scatto
open Scatto.xcodeproj
Set the signing team to your Apple ID for the three targets: Scatto, ScattoShareExtension, ScattoWidgetExtension. Bundle IDs are namespaced under de.hever.scatto.
Push notifications are optional. They require the companion scatto-push-relay running on your own host (with an APNs auth key) and its base URL + subscribe token wired into AppSecrets. Without it, everything else — timelines, uploads, video, interaction sounds when the app is open — works normally.