Files
andros b04da850e4 EULA gate, block accounts and 24h moderation commitment
App Store guideline 1.2 requires four UGC controls; this lands three of
them and updates the privacy policy with the 24 h response commitment.

EULA gate: a fullScreenCover is shown over RootView whenever the stored
eulaAcceptedVersion is below EULAView.currentVersion. The view links to
the Apple Standard EULA and spells out the project-specific zero-tolerance
clause that the rejection letter quoted verbatim. Bumping currentVersion
forces re-acceptance after the terms change. Settings -> About -> "Terms
of Use" reopens the same view in read-only mode.

Block accounts: BlockList is an @Observable singleton that persists a
JSON array of FollowCoordinator-normalised feed URLs in UserDefaults.
ProfileView gains a Block / Unblock button next to View social.org for
non-own profiles. Settings exposes the list with an Unblock action per
row. Filtering happens at render time in TimelineView and NotificationsView
so blocking from a profile removes content from the feed instantly, as
the rejection letter requires; load-time filtering in the view models
stays as defense in depth.

PRIVACY.md grows a "Moderation and reports" section that names the three
tools, commits to acting on reports within 24 hours, and restates the
zero-tolerance policy. Apple expects this text in the public privacy
policy; the same wording will go into the App Review Information Notes
when we resubmit.

Report post and the in-app mirror of the 24 h paragraph remain open;
both depend on the developer-notification channel decision.
2026-05-02 08:45:41 +02:00

91 lines
3.9 KiB
Swift

import SwiftUI
/// Terms of Use modal shown before any user-generated content is reachable.
/// Required by App Store guideline 1.2: "users agree to terms (EULA) and
/// these terms must make it clear that there is no tolerance for
/// objectionable content or abusive users".
///
/// Acceptance is recorded as `eulaAcceptedVersion` in `UserDefaults`. Bumping
/// `currentVersion` forces re-acceptance after the terms change.
struct EULAView: View {
/// Bumped when the project-specific paragraph below changes; keeps the
/// gate behaviour explicit instead of hashing the body text.
static let currentVersion: Int = 1
/// `nil` in the gate flow (full screen, requires acceptance).
/// Non-nil in read-only mode from Settings (just a Close button).
var onDismiss: (() -> Void)?
var body: some View {
NavigationStack {
ScrollView {
VStack(alignment: .leading, spacing: 16) {
Text("Terms of Use")
.font(.title.weight(.bold))
Text("By using Org Social you agree to the following:")
.font(.headline)
Text("""
Org Social is a federated client for plain-text social.org \
feeds hosted on servers chosen by the user. The app does \
not host, index or store any of the content shown in the \
timeline; reads come from public URLs you explicitly \
follow, and writes go to the server you configured.
Zero tolerance for objectionable content or abusive users. \
Posting threats, harassment, hate speech, sexual content \
involving minors, doxxing, intellectual-property \
infringement, or content that violates applicable law \
is grounds for being blocked and reported. We act on \
reports of objectionable content within 24 hours by \
removing the offending post from our local index and \
ejecting the author from features that depend on us.
Filtering, blocking and reporting tools are available \
inside the app:
\u{2022} Settings \u{2192} Muted words: keyword filter
\u{2022} Each profile: Block this account
\u{2022} Each post menu: Report this post
The Apple Standard EULA also applies. Read the full \
text at the link below.
""")
.font(.body)
Link("Apple Standard EULA",
destination: URL(string: "https://www.apple.com/legal/internet-services/itunes/dev/stdeula/")!)
.font(.body.weight(.medium))
Spacer(minLength: 0)
}
.padding(20)
}
.toolbar {
if let onDismiss {
ToolbarItem(placement: .cancellationAction) {
Button("Close") { onDismiss() }
}
}
}
.safeAreaInset(edge: .bottom) {
if onDismiss == nil {
Button {
UserDefaults.standard.set(EULAView.currentVersion, forKey: "eulaAcceptedVersion")
} label: {
Text("I Agree")
.font(.body.weight(.semibold))
.frame(maxWidth: .infinity)
.padding(.vertical, 12)
}
.buttonStyle(.borderedProminent)
.padding(.horizontal, 20)
.padding(.bottom, 12)
.background(.thinMaterial)
}
}
}
.interactiveDismissDisabled(onDismiss == nil)
}
}