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>
107 lines
3.0 KiB
TypeScript
107 lines
3.0 KiB
TypeScript
import { create } from 'zustand';
|
|
|
|
export type CommunityCategory = 'all' | 'games' | 'domain_vote' | 'lyra' | 'rebreak';
|
|
|
|
export interface CommunityPostAuthor {
|
|
id: string | null;
|
|
username: string;
|
|
nickname: string;
|
|
avatar: string | null;
|
|
plan: string;
|
|
tier?: string;
|
|
}
|
|
|
|
export interface CommunityPost {
|
|
id: string;
|
|
category: string;
|
|
content: string;
|
|
imageUrl?: string | null;
|
|
likesCount: number;
|
|
dislikesCount: number;
|
|
commentsCount: number;
|
|
repostsCount: number;
|
|
isAnonymous: boolean;
|
|
createdAt: string;
|
|
userLike: 'like' | 'dislike' | null;
|
|
isBot?: boolean;
|
|
botType?: 'lyra' | 'rebreak';
|
|
gameName?: string | null;
|
|
challengeId?: string | null;
|
|
challengeStatus?: 'OPEN' | 'ACTIVE' | 'FINISHED' | 'CANCELLED' | null;
|
|
opponentName?: string | null;
|
|
isLive?: boolean;
|
|
i18nKey?: string | null;
|
|
userVote?: 'yes' | 'no' | null;
|
|
submission?: {
|
|
id: string;
|
|
domain: string;
|
|
status: 'pending' | 'approved' | 'rejected' | 'in_review';
|
|
yesVotes: number;
|
|
noVotes: number;
|
|
reviewedAt?: string | null;
|
|
yesVoters?: Array<{ id: string; nickname: string; avatar: string | null }>;
|
|
noVoters?: Array<{ id: string; nickname: string; avatar: string | null }>;
|
|
} | null;
|
|
author: CommunityPostAuthor;
|
|
repostOf?: {
|
|
author: CommunityPostAuthor;
|
|
content: string;
|
|
imageUrl?: string | null;
|
|
} | null;
|
|
}
|
|
|
|
export interface CommunityComment {
|
|
id: string;
|
|
content: string;
|
|
createdAt: string;
|
|
parentCommentId: string | null;
|
|
authorNickname: string;
|
|
authorAvatar: string | null;
|
|
authorId: string | null;
|
|
likesCount: number;
|
|
userLike: boolean;
|
|
}
|
|
|
|
type CommunityState = {
|
|
activeCategory: CommunityCategory;
|
|
setCategory: (cat: CommunityCategory) => void;
|
|
optimisticLikes: Record<string, { delta: number; userLike: 'like' | null }>;
|
|
applyOptimisticLike: (postId: string, currentLike: 'like' | null, currentCount: number) => { newLike: 'like' | null; newCount: number };
|
|
revertOptimisticLike: (postId: string) => void;
|
|
clearOptimisticLike: (postId: string) => void;
|
|
};
|
|
|
|
export const useCommunityStore = create<CommunityState>((set, get) => ({
|
|
activeCategory: 'all',
|
|
optimisticLikes: {},
|
|
|
|
setCategory: (cat) => set({ activeCategory: cat }),
|
|
|
|
applyOptimisticLike: (postId, currentLike, currentCount) => {
|
|
const isLiked = currentLike === 'like';
|
|
const newLike: 'like' | null = isLiked ? null : 'like';
|
|
const newCount = isLiked ? Math.max(0, currentCount - 1) : currentCount + 1;
|
|
set((s) => ({
|
|
optimisticLikes: {
|
|
...s.optimisticLikes,
|
|
[postId]: { delta: newCount - currentCount, userLike: newLike },
|
|
},
|
|
}));
|
|
return { newLike, newCount };
|
|
},
|
|
|
|
revertOptimisticLike: (postId) => {
|
|
set((s) => {
|
|
const { [postId]: _, ...rest } = s.optimisticLikes;
|
|
return { optimisticLikes: rest };
|
|
});
|
|
},
|
|
|
|
clearOptimisticLike: (postId) => {
|
|
set((s) => {
|
|
const { [postId]: _, ...rest } = s.optimisticLikes;
|
|
return { optimisticLikes: rest };
|
|
});
|
|
},
|
|
}));
|