chahinebrini 89e4e3481b feat(calls): Phase 0 — calls_enabled opt-out + canCall guard (mutual-follow); DM UI batch
Backend (voice-call groundwork, no call engine yet):
- Profile.callsEnabled (Boolean default true) + migration
- canCall(caller,callee): mutual-follow AND callee.callsEnabled — server-side hard guard
- POST /api/me/calls-enabled (opt-out toggle), GET /api/chat/can-call/:userId
- expose callsEnabled in /api/auth/me

Frontend:
- "Allow calls" toggle in Profile privacy section (default on, optimistic+rollback)
- Me.callsEnabled + i18n DE/EN/FR/AR

Bundled DM UI work from this session:
- image lightbox is now a swipeable carousel over all shared images (+ counter)
- keyboard stays open after sending (input ref refocus)
- voice notes: Instagram-style waveforms (own=white/mint, other=black/grey),
  removed the blue progress dot; lazy-load expo-media-library with clean fallback
- expo-linear-gradient + expo-media-library deps

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 21:14:31 +02:00

105 lines
3.2 KiB
TypeScript

import { usePrisma } from "../utils/prisma";
export async function getFollowRelation(
followerId: string,
followingId: string,
) {
const db = usePrisma();
return db.userFollow.findUnique({
where: { followerId_followingId: { followerId, followingId } },
});
}
export async function createFollow(followerId: string, followingId: string) {
const db = usePrisma();
const exists = await db.userFollow.findUnique({
where: { followerId_followingId: { followerId, followingId } },
});
if (exists) return;
await db.userFollow.create({ data: { followerId, followingId } });
await db.profile.update({
where: { id: followingId },
data: { followersCount: { increment: 1 } },
});
}
export async function deleteFollow(followerId: string, followingId: string) {
const db = usePrisma();
const exists = await db.userFollow.findUnique({
where: { followerId_followingId: { followerId, followingId } },
});
if (!exists) return;
await db.userFollow.delete({
where: { followerId_followingId: { followerId, followingId } },
});
// Nie unter 0
const profile = await db.profile.findUnique({
where: { id: followingId },
select: { followersCount: true },
});
if (profile && profile.followersCount > 0) {
await db.profile.update({
where: { id: followingId },
data: { followersCount: { decrement: 1 } },
});
}
}
/** Gibt ein Set der userIds zurück, denen followerId bereits folgt (Batch-Variante). */
export async function getFollowingSet(
followerId: string,
followingIds: string[],
): Promise<Set<string>> {
if (followingIds.length === 0) return new Set();
const db = usePrisma();
const rows = await db.userFollow.findMany({
where: { followerId, followingId: { in: followingIds } },
select: { followingId: true },
});
return new Set(rows.map((r) => r.followingId));
}
export async function getProfileWithFollowers(userId: string) {
const db = usePrisma();
return db.profile.findUnique({
where: { id: userId },
select: {
followersCount: true,
username: true,
avatar: true,
nickname: true,
},
});
}
/**
* Voice-Call erlaubt? Nur wenn (1) beide sich GEGENSEITIG folgen UND (2) der
* Angerufene Anrufe nicht deaktiviert hat (callsEnabled). Server-seitige
* Hard-Schranke — die UI blendet den Call-Button zwar aus, aber der Call-Start
* MUSS das hier zusätzlich prüfen.
*/
export async function canCall(
callerId: string,
calleeId: string,
): Promise<boolean> {
if (!callerId || !calleeId || callerId === calleeId) return false;
const db = usePrisma();
const [callerFollowsCallee, calleeFollowsCaller, callee] = await Promise.all([
db.userFollow.findUnique({
where: { followerId_followingId: { followerId: callerId, followingId: calleeId } },
select: { followerId: true },
}),
db.userFollow.findUnique({
where: { followerId_followingId: { followerId: calleeId, followingId: callerId } },
select: { followerId: true },
}),
db.profile.findUnique({
where: { id: calleeId },
select: { callsEnabled: true },
}),
]);
return (
!!callerFollowsCallee && !!calleeFollowsCaller && (callee?.callsEnabled ?? true)
);
}