Voice Notes (DM): - WhatsApp-style voice recording bar (shared VoiceRecordingBar component) - Audio bubbles: 80 fixed-2dp bars (Instagram-style thin), space-between layout, deterministic waveform, moving blue position dot, WA gray bar colors - Cancel flash fix: setIsVoiceRecording delayed 350ms so trash flash is visible - Mic button 44pt (Apple min), hitSlop on all recording controls - startReply shows 🎤/📷 label for voice/image instead of empty Chat list: - lastAttachmentType from backend (getDmConversations now selects attachmentType) - Shows '🎤 Sprachnachricht' / '📷 Foto' / '📎 Medien' as fallback per type - User search second stage: GET /api/users/search?q= + debounced frontend section - Push preview: audio → '🎤 Sprachnachricht', image → '📷 Foto' (was '📎 Anhang') Blocker iOS Layer 3 (Screen Time): - ScreentimePasscodeCard visible in locked-in state (was hidden once both layers active) - Confirmed status loaded from backend on mount - Numbered step instructions (iOS has no deep link to passcode dialog) - Guard: only for unsupervised VPN+FC path (!mdmManaged && !nefilterActive) - URL fallback: App-Prefs:SCREEN_TIME → App-Prefs:root=SCREEN_TIME → openSettings DiGA Milestone Modal: - Day 3/7/10 celebratory bottom sheet with soft demographic data ask - Per-user/milestone AsyncStorage tracking, never shows if demographics filled - Opens DemographicsAccordion in profile via ?openDemo=1 param Lyra coach: contextual DiGA demographic nudge (optional, positive moments only) i18n: DE/EN/FR/AR for voice_message, photo, media_sent, mic_access, diga_milestone, screentime steps, chat search strings Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
46 lines
1.6 KiB
TypeScript
46 lines
1.6 KiB
TypeScript
import { getDmConversations, countUnreadDms } from "../../db/chat";
|
|
import { getProfile } from "../../db/profile";
|
|
import { getUsersMeta } from "../../utils/getUsersMeta";
|
|
|
|
/** GET /api/chat/dm-conversations */
|
|
export default defineEventHandler(async (event) => {
|
|
const user = await requireUser(event);
|
|
|
|
const [messages, unreadByPartner] = await Promise.all([
|
|
getDmConversations(user.id),
|
|
countUnreadDms(user.id),
|
|
]);
|
|
|
|
if (messages.length === 0) return [];
|
|
|
|
const partnerMap = new Map<string, (typeof messages)[0]>();
|
|
for (const msg of messages) {
|
|
const partnerId = msg.senderId === user.id ? msg.receiverId : msg.senderId;
|
|
if (!partnerMap.has(partnerId)) partnerMap.set(partnerId, msg);
|
|
}
|
|
|
|
const partnerIds = Array.from(partnerMap.keys());
|
|
const [profileResults, metaMap] = await Promise.all([
|
|
Promise.all(partnerIds.map((id) => getProfile(id))),
|
|
getUsersMeta(partnerIds),
|
|
]);
|
|
const profileMap = new Map(
|
|
profileResults.filter(Boolean).map((p) => [p!.id, p!]),
|
|
);
|
|
|
|
return Array.from(partnerMap.entries()).map(([partnerId, lastMsg]) => {
|
|
const p = profileMap.get(partnerId);
|
|
const meta = metaMap[partnerId] ?? { nickname: null, avatar: null };
|
|
return {
|
|
partnerId,
|
|
partnerName: meta.nickname ?? p?.username ?? "Anonym",
|
|
partnerAvatar: meta.avatar ?? null,
|
|
lastMessage: lastMsg.content.slice(0, 60),
|
|
lastAttachmentType: lastMsg.attachmentType ?? null,
|
|
lastMessageAt: lastMsg.createdAt,
|
|
isOwn: lastMsg.senderId === user.id,
|
|
unreadCount: unreadByPartner[partnerId] ?? 0,
|
|
};
|
|
});
|
|
});
|