chahinebrini b31066a04c feat(chat): native action sheet + Insta-style heart for DM messages
- ChatBubble: useActionSheet replaces custom Modal (native iOS popup, Android bottom sheet)
- DM mode (isDM prop): hides like-count, shows Insta-style heart badge under bubble when liked
- Group chat unchanged
- Cleanup: remove unused Modal/Platform imports, sheet styles, actionsOpen state
- deploy.sh: auto-detect ANDROID_HOME + auto-create local.properties for local Gradle
- NEXT_RELEASE.md: DM reactions release note
- Includes other staged work across binder-mac, marketing, ops/mdm, ios/
2026-05-30 09:14:32 +02:00

114 lines
3.3 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;
composeInputFocused: boolean;
setComposeInputFocused: (focused: boolean) => 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;
reset: () => void;
};
export const useCommunityStore = create<CommunityState>((set, get) => ({
activeCategory: 'all',
composeInputFocused: false,
optimisticLikes: {},
setCategory: (cat) => set({ activeCategory: cat }),
setComposeInputFocused: (focused) => set({ composeInputFocused: focused }),
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 };
});
},
reset: () => set({ activeCategory: 'all', composeInputFocused: false, optimisticLikes: {} }),
}));