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>
439 lines
12 KiB
TypeScript
439 lines
12 KiB
TypeScript
import { usePrisma } from "../utils/prisma";
|
|
|
|
// ─── Posts ────────────────────────────────────────────────────────────────────
|
|
|
|
export async function getPosts(
|
|
category: string,
|
|
page: number,
|
|
limit: number,
|
|
currentUserId: string | null,
|
|
filterUserId?: string | null,
|
|
) {
|
|
const db = usePrisma();
|
|
const offset = (page - 1) * limit;
|
|
|
|
const where: any = { isModerated: false };
|
|
if (category !== "all") {
|
|
if (category === "games") {
|
|
where.category = { in: ["game_share", "challenge"] };
|
|
} else {
|
|
where.category = category;
|
|
}
|
|
}
|
|
if (filterUserId) {
|
|
where.userId = filterUserId;
|
|
}
|
|
|
|
const posts = await db.communityPost.findMany({
|
|
where,
|
|
orderBy: { createdAt: "desc" },
|
|
skip: offset,
|
|
take: limit,
|
|
include: {
|
|
author: {
|
|
select: {
|
|
id: true,
|
|
username: true,
|
|
nickname: true,
|
|
avatar: true,
|
|
plan: true,
|
|
},
|
|
},
|
|
repostOf: {
|
|
include: {
|
|
author: {
|
|
select: {
|
|
id: true,
|
|
username: true,
|
|
nickname: true,
|
|
avatar: true,
|
|
plan: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
// Batch: UserScore (tier) für alle Autoren laden
|
|
const authorUserIds = [
|
|
...new Set(
|
|
posts
|
|
.flatMap((p) => [p.userId, p.repostOf?.userId])
|
|
.filter((id): id is string => !!id),
|
|
),
|
|
];
|
|
let userScores: Record<string, string> = {};
|
|
if (authorUserIds.length > 0) {
|
|
const scores = await db.userScore.findMany({
|
|
where: { userId: { in: authorUserIds } },
|
|
select: { userId: true, tier: true },
|
|
});
|
|
for (const s of scores) userScores[s.userId] = s.tier;
|
|
}
|
|
|
|
// Eigene Likes laden wenn eingeloggt
|
|
let userLikes: Record<string, "like" | "dislike"> = {};
|
|
if (currentUserId && posts.length > 0) {
|
|
const postIds = posts.map((p) => p.id);
|
|
const likes = await db.postLike.findMany({
|
|
where: { userId: currentUserId, postId: { in: postIds } },
|
|
select: { postId: true, type: true },
|
|
});
|
|
for (const l of likes) {
|
|
userLikes[l.postId] = l.type as "like" | "dislike";
|
|
}
|
|
}
|
|
|
|
// Challenge-Status für Challenge-Posts laden
|
|
const challengeIds = posts
|
|
.map((p) => (p as any).challengeId)
|
|
.filter((id): id is string => !!id);
|
|
let challengeStatuses: Record<
|
|
string,
|
|
{
|
|
status: string;
|
|
opponentId: string | null;
|
|
opponentName: string | null;
|
|
gameType: string | null;
|
|
isLive: boolean;
|
|
}
|
|
> = {};
|
|
if (challengeIds.length > 0) {
|
|
const challenges = await db.gameChallenge.findMany({
|
|
where: { id: { in: challengeIds } },
|
|
select: {
|
|
id: true,
|
|
status: true,
|
|
opponentId: true,
|
|
opponentName: true,
|
|
gameType: true,
|
|
isLive: true,
|
|
},
|
|
});
|
|
for (const c of challenges) {
|
|
challengeStatuses[c.id] = {
|
|
status: c.status,
|
|
opponentId: c.opponentId,
|
|
opponentName: (c as any).opponentName ?? null,
|
|
gameType: (c as any).gameType ?? null,
|
|
isLive: (c as any).isLive ?? false,
|
|
};
|
|
}
|
|
}
|
|
|
|
// Domain-Submission-Daten für domain_vote Posts laden
|
|
const domainVotePostIds = posts
|
|
.filter((p) => p.category === "domain_vote")
|
|
.map((p) => p.id);
|
|
type SubEntry = {
|
|
id: string;
|
|
domain: string;
|
|
yesVotes: number;
|
|
noVotes: number;
|
|
status: string;
|
|
reviewedAt: Date | null;
|
|
};
|
|
let domainSubmissions: Record<string, SubEntry> = {};
|
|
let userDomainVotes: Record<string, "yes" | "no"> = {};
|
|
let submissionVoters: Record<
|
|
string,
|
|
{
|
|
yes: { id: string; nickname: string; avatar: string | null }[];
|
|
no: { id: string; nickname: string; avatar: string | null }[];
|
|
}
|
|
> = {};
|
|
|
|
if (domainVotePostIds.length > 0) {
|
|
const subs = await db.domainSubmission.findMany({
|
|
where: { postId: { in: domainVotePostIds } },
|
|
select: {
|
|
id: true,
|
|
postId: true,
|
|
domain: true,
|
|
yesVotes: true,
|
|
noVotes: true,
|
|
status: true,
|
|
reviewedAt: true,
|
|
},
|
|
});
|
|
for (const s of subs) {
|
|
if (s.postId)
|
|
domainSubmissions[s.postId] = {
|
|
id: s.id,
|
|
domain: s.domain,
|
|
yesVotes: s.yesVotes,
|
|
noVotes: s.noVotes,
|
|
status: s.status,
|
|
reviewedAt: s.reviewedAt ?? null,
|
|
};
|
|
}
|
|
if (currentUserId && subs.length > 0) {
|
|
const votes = await db.domainVote.findMany({
|
|
where: {
|
|
userId: currentUserId,
|
|
submissionId: { in: subs.map((s) => s.id) },
|
|
},
|
|
select: { submissionId: true, vote: true },
|
|
});
|
|
const subIdToPostId = Object.fromEntries(
|
|
Object.entries(domainSubmissions).map(([pid, s]) => [s.id, pid]),
|
|
);
|
|
for (const v of votes) {
|
|
const pid = subIdToPostId[v.submissionId];
|
|
if (pid) userDomainVotes[pid] = v.vote as "yes" | "no";
|
|
}
|
|
}
|
|
|
|
// Batch: DomainVote voters for domain submissions
|
|
const submissionIds = Object.values(domainSubmissions)
|
|
.map((s) => s.id)
|
|
.filter(Boolean);
|
|
if (submissionIds.length > 0) {
|
|
const votes = await db.domainVote.findMany({
|
|
where: { submissionId: { in: submissionIds } },
|
|
select: { submissionId: true, vote: true, userId: true },
|
|
});
|
|
const voterIds = [...new Set(votes.map((v) => v.userId))];
|
|
let voterProfiles: Record<
|
|
string,
|
|
{ nickname: string | null; avatar: string | null }
|
|
> = {};
|
|
if (voterIds.length > 0) {
|
|
const profiles = await db.profile.findMany({
|
|
where: { id: { in: voterIds } },
|
|
select: { id: true, nickname: true, avatar: true },
|
|
});
|
|
for (const p of profiles)
|
|
voterProfiles[p.id] = { nickname: p.nickname, avatar: p.avatar };
|
|
}
|
|
for (const v of votes) {
|
|
if (!submissionVoters[v.submissionId])
|
|
submissionVoters[v.submissionId] = { yes: [], no: [] };
|
|
const profile = voterProfiles[v.userId];
|
|
const voter = {
|
|
id: v.userId,
|
|
nickname: profile?.nickname ?? "Nutzer",
|
|
avatar: profile?.avatar ?? null,
|
|
};
|
|
if (v.vote === "yes") submissionVoters[v.submissionId].yes.push(voter);
|
|
else submissionVoters[v.submissionId].no.push(voter);
|
|
}
|
|
}
|
|
}
|
|
|
|
return {
|
|
posts,
|
|
userLikes,
|
|
challengeStatuses,
|
|
domainSubmissions,
|
|
userDomainVotes,
|
|
userScores,
|
|
submissionVoters,
|
|
};
|
|
}
|
|
|
|
export async function getPostById(postId: string) {
|
|
const db = usePrisma();
|
|
return db.communityPost.findUnique({
|
|
where: { id: postId },
|
|
include: {
|
|
author: {
|
|
select: {
|
|
id: true,
|
|
username: true,
|
|
nickname: true,
|
|
avatar: true,
|
|
plan: true,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
}
|
|
|
|
export async function createPost(
|
|
userId: string,
|
|
category: string,
|
|
content: string,
|
|
imageUrl?: string,
|
|
gameName?: string | null,
|
|
i18nKey?: string | null,
|
|
) {
|
|
const db = usePrisma();
|
|
return db.communityPost.create({
|
|
data: {
|
|
userId,
|
|
category,
|
|
content,
|
|
imageUrl: imageUrl || null,
|
|
gameName: gameName ?? null,
|
|
i18nKey: i18nKey ?? null,
|
|
isAnonymous: false,
|
|
isModerated: false,
|
|
},
|
|
include: {
|
|
author: {
|
|
select: {
|
|
id: true,
|
|
username: true,
|
|
nickname: true,
|
|
avatar: true,
|
|
plan: true,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
}
|
|
|
|
export async function deleteUserPosts(userId: string) {
|
|
const db = usePrisma();
|
|
return db.communityPost.deleteMany({ where: { userId } });
|
|
}
|
|
|
|
// ─── Likes ───────────────────────────────────────────────────────────────────
|
|
|
|
export async function getPostLike(userId: string, postId: string) {
|
|
const db = usePrisma();
|
|
return db.postLike.findUnique({
|
|
where: { userId_postId: { userId, postId } },
|
|
select: { type: true },
|
|
});
|
|
}
|
|
|
|
export async function setPostLike(
|
|
userId: string,
|
|
postId: string,
|
|
type: "like" | "dislike",
|
|
) {
|
|
const db = usePrisma();
|
|
return db.postLike.upsert({
|
|
where: { userId_postId: { userId, postId } },
|
|
create: { userId, postId, type },
|
|
update: { type },
|
|
});
|
|
}
|
|
|
|
export async function deletePostLike(userId: string, postId: string) {
|
|
const db = usePrisma();
|
|
return db.postLike.delete({
|
|
where: { userId_postId: { userId, postId } },
|
|
});
|
|
}
|
|
|
|
export async function countPostLikes(postId: string) {
|
|
const db = usePrisma();
|
|
const counts = await db.postLike.groupBy({
|
|
by: ["type"],
|
|
where: { postId },
|
|
_count: { type: true },
|
|
});
|
|
const likes = counts.find((c) => c.type === "like")?._count.type ?? 0;
|
|
const dislikes = counts.find((c) => c.type === "dislike")?._count.type ?? 0;
|
|
return { likes, dislikes };
|
|
}
|
|
|
|
export async function syncPostLikeCounts(
|
|
postId: string,
|
|
likes: number,
|
|
dislikes: number,
|
|
) {
|
|
const db = usePrisma();
|
|
return db.communityPost.update({
|
|
where: { id: postId },
|
|
data: { likesCount: likes, dislikesCount: dislikes },
|
|
});
|
|
}
|
|
|
|
// ─── Comments (replies) ──────────────────────────────────────────────────────
|
|
|
|
export async function getCommentsByPost(
|
|
postId: string,
|
|
currentUserId: string | null,
|
|
) {
|
|
const db = usePrisma();
|
|
const comments = await db.communityReply.findMany({
|
|
where: { postId },
|
|
orderBy: { createdAt: "asc" },
|
|
take: 200,
|
|
include: {
|
|
author: {
|
|
select: { id: true, username: true, nickname: true, avatar: true },
|
|
},
|
|
},
|
|
});
|
|
|
|
let userLikes = new Set<string>();
|
|
if (currentUserId && comments.length > 0) {
|
|
const commentIds = comments.map((c) => c.id);
|
|
const likes = await db.commentLike.findMany({
|
|
where: { userId: currentUserId, commentId: { in: commentIds } },
|
|
select: { commentId: true },
|
|
});
|
|
for (const l of likes) {
|
|
userLikes.add(l.commentId);
|
|
}
|
|
}
|
|
|
|
return { comments, userLikes };
|
|
}
|
|
|
|
export async function createComment(
|
|
userId: string,
|
|
postId: string,
|
|
content: string,
|
|
parentReplyId: string | null,
|
|
) {
|
|
const db = usePrisma();
|
|
const [reply] = await Promise.all([
|
|
db.communityReply.create({
|
|
data: { userId, postId, content, parentReplyId, isAnonymous: false },
|
|
select: {
|
|
id: true,
|
|
content: true,
|
|
createdAt: true,
|
|
likesCount: true,
|
|
parentReplyId: true,
|
|
},
|
|
}),
|
|
db.communityPost.update({
|
|
where: { id: postId },
|
|
data: { commentsCount: { increment: 1 } },
|
|
}),
|
|
]);
|
|
return reply;
|
|
}
|
|
|
|
// ─── Comment Likes ────────────────────────────────────────────────────────────
|
|
|
|
export async function getCommentLike(userId: string, commentId: string) {
|
|
const db = usePrisma();
|
|
return db.commentLike.findUnique({
|
|
where: { userId_commentId: { userId, commentId } },
|
|
});
|
|
}
|
|
|
|
export async function createCommentLike(userId: string, commentId: string) {
|
|
const db = usePrisma();
|
|
return db.commentLike.create({ data: { userId, commentId } });
|
|
}
|
|
|
|
export async function deleteCommentLike(userId: string, commentId: string) {
|
|
const db = usePrisma();
|
|
return db.commentLike.delete({
|
|
where: { userId_commentId: { userId, commentId } },
|
|
});
|
|
}
|
|
|
|
export async function getCommentLikeCount(commentId: string) {
|
|
const db = usePrisma();
|
|
return db.commentLike.count({ where: { commentId } });
|
|
}
|
|
|
|
export async function syncCommentLikeCount(commentId: string, count: number) {
|
|
const db = usePrisma();
|
|
return db.communityReply.update({
|
|
where: { id: commentId },
|
|
data: { likesCount: count },
|
|
});
|
|
}
|