Android-Onboarding (Platform.OS dispatch in ProtectionSlide):
- Neue Phasen für Android: preexplain_vpn → preexplain_a11y → a11y_pending
- AppState-Listener: nach Settings-Rückkehr auto-poll isAccessibilityEnabled
→ wenn live, armTamperLock + finish (kein Fokus-Klick nötig)
- onboardingAssets: 8 neue Mappings (android_vpn + android_a11y × 4 Locales)
- Screenshots: vpn-permission + a11y-rebreak-row pro Locale
- Locale-Keys: protection_url_android, protection_lock_android, cta_open_a11y,
cta_check_a11y, dialog_button_vpn_ok, dialog_button_a11y_toggle, tap_marker_hint_*
Lyra-Post i18n Phase 1 (Scaffold, feature-flag OFF by default):
- schema.prisma: CommunityPost.i18nKey String? (nullable)
- migration 20260517_add_lyra_post_i18n_key: ALTER TABLE ADD COLUMN i18n_key
(NICHT auto-deployed — `prisma migrate deploy` als separater Step)
- server/lib/lyraPostCatalog.ts: 15 Templates skelettiert + pickRandomTemplate
- cron/lyra-post: USE_TEMPLATE_CATALOG=true Branch → speichert i18nKey;
default false → LLM-Path unverändert (zero-risk-deployment)
- community.createPost: optionaler i18nKey-Parameter
- posts.get: i18nKey in API-Response
- PostCard: 3-Zeilen-Branch — i18nKey ? t('lyra_posts.'+id) : content
- stores/community: i18nKey?: string|null im Interface
- de.json: lyra_posts-Block mit 15 IDs + DE-Texten
Single-Banner-Verhalten auf Android verifiziert:
lockedIn=urlFilter && appDeletionLock funktioniert weiter — auf Android
alias appDeletionLock ← tamperLock; onboarding arms tamperLock, also
nach onboarding-done direkt ProtectionLockedCard sichtbar.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
131 lines
4.5 KiB
TypeScript
131 lines
4.5 KiB
TypeScript
import { getPosts } from "../../db/community";
|
|
import { getFollowingSet } from "../../db/social";
|
|
|
|
/** GET /api/community/posts?category=all&page=1&limit=20 */
|
|
export default defineEventHandler(async (event) => {
|
|
const config = useRuntimeConfig();
|
|
const lyraBotUserId = config.lyraBotUserId || null;
|
|
const rebreakBotUserId = config.rebreakBotUserId || null;
|
|
const query = getQuery(event);
|
|
const category = (query.category as string) || "all";
|
|
const page = Math.max(1, parseInt((query.page as string) || "1"));
|
|
const limit = Math.min(50, parseInt((query.limit as string) || "20"));
|
|
|
|
// Lyra / ReBreak → nach userId filtern
|
|
let filterUserId: string | null = null;
|
|
let dbCategory = category;
|
|
if (category === "lyra") {
|
|
filterUserId = lyraBotUserId;
|
|
dbCategory = "all";
|
|
} else if (category === "rebreak") {
|
|
filterUserId = rebreakBotUserId;
|
|
dbCategory = "all";
|
|
}
|
|
|
|
// Auth-User optional (Gäste können auch lesen)
|
|
let currentUserId: string | null = null;
|
|
try {
|
|
const u = await requireUser(event);
|
|
currentUserId = u.id;
|
|
} catch {}
|
|
|
|
const {
|
|
posts,
|
|
userLikes,
|
|
challengeStatuses,
|
|
domainSubmissions,
|
|
userDomainVotes,
|
|
userScores,
|
|
submissionVoters,
|
|
} = await getPosts(dbCategory, page, limit, currentUserId, filterUserId);
|
|
|
|
// Batch: isFollowing für alle Autoren
|
|
const authorIds = [
|
|
...new Set(posts.map((p) => p.userId).filter((id): id is string => !!id)),
|
|
];
|
|
const followingSet =
|
|
currentUserId && authorIds.length > 0
|
|
? await getFollowingSet(currentUserId, authorIds)
|
|
: new Set<string>();
|
|
|
|
return posts.map((p) => {
|
|
const a = p.author;
|
|
return {
|
|
id: p.id,
|
|
category: p.category,
|
|
content:
|
|
p.category === "game_share"
|
|
? p.content.split("\n").slice(1).join("\n").trim()
|
|
: p.content,
|
|
imageUrl: (p as any).imageUrl ?? null,
|
|
challengeId: (p as any).challengeId ?? null,
|
|
challengeStatus: (p as any).challengeId
|
|
? challengeStatuses[(p as any).challengeId]?.status ?? "OPEN"
|
|
: null,
|
|
gameName: (p as any).challengeId
|
|
? challengeStatuses[(p as any).challengeId]?.gameType ?? null
|
|
: (p as any).gameName ?? (p.category === "game_share"
|
|
? p.content.split("\n")[0] ?? null
|
|
: null),
|
|
opponentName: (p as any).challengeId
|
|
? challengeStatuses[(p as any).challengeId]?.opponentName ?? null
|
|
: null,
|
|
isLive: (p as any).challengeId
|
|
? challengeStatuses[(p as any).challengeId]?.isLive ?? false
|
|
: false,
|
|
likesCount: p.likesCount ?? 0,
|
|
dislikesCount: p.dislikesCount ?? 0,
|
|
commentsCount: p.commentsCount ?? 0,
|
|
repostsCount: (p as any).repostsCount ?? 0,
|
|
isAnonymous: p.isAnonymous,
|
|
createdAt: p.createdAt,
|
|
userLike: userLikes[p.id] ?? null,
|
|
submission: domainSubmissions[p.id]
|
|
? {
|
|
...domainSubmissions[p.id],
|
|
yesVoters: submissionVoters[domainSubmissions[p.id].id]?.yes ?? [],
|
|
noVoters: submissionVoters[domainSubmissions[p.id].id]?.no ?? [],
|
|
}
|
|
: null,
|
|
userVote: userDomainVotes[p.id] ?? null,
|
|
i18nKey: (p as any).i18nKey ?? null,
|
|
repostOfId: (p as any).repostOfId ?? null,
|
|
repostOf: (p as any).repostOf
|
|
? {
|
|
id: (p as any).repostOf.id,
|
|
content: (p as any).repostOf.content,
|
|
imageUrl: (p as any).repostOf.imageUrl ?? null,
|
|
author: {
|
|
id: (p as any).repostOf.userId ?? null,
|
|
nickname:
|
|
(p as any).repostOf.author?.nickname ??
|
|
(p as any).repostOf.author?.username ??
|
|
"Nutzer",
|
|
avatar: (p as any).repostOf.author?.avatar ?? null,
|
|
plan: (p as any).repostOf.author?.plan ?? "free",
|
|
tier: userScores[(p as any).repostOf.userId ?? ""] ?? "beginner",
|
|
},
|
|
}
|
|
: null,
|
|
author: {
|
|
id: p.userId ?? null,
|
|
username: a?.username ?? "Nutzer",
|
|
nickname: a?.nickname ?? a?.username ?? "Nutzer",
|
|
avatar: a?.avatar ?? null,
|
|
plan: (a as any)?.plan ?? "free",
|
|
tier: userScores[p.userId ?? ""] ?? "beginner",
|
|
isFollowing: p.userId ? followingSet.has(p.userId) : false,
|
|
},
|
|
isBot:
|
|
!!(lyraBotUserId && p.userId === lyraBotUserId) ||
|
|
!!(rebreakBotUserId && p.userId === rebreakBotUserId),
|
|
botType:
|
|
lyraBotUserId && p.userId === lyraBotUserId
|
|
? "lyra"
|
|
: rebreakBotUserId && p.userId === rebreakBotUserId
|
|
? "rebreak"
|
|
: undefined,
|
|
};
|
|
});
|
|
});
|