UX: landscape result screen, chat emojis, unread badges, remove XP text
- PostMatchRewardsModal: short-height (landscape) compaction so the win/forfeit result fits without overflow (smaller emoji/coins/padding, max-h 94dvh, wider). - Chat: emoji/sticker picker (owned reactions) — tap to send; hidden on focus. - Unread messages: online-store now tracks a total `unread` (from listConversations); NavRail Friends icon shows a badge (unread + requests), refreshed every 12s on every screen; Friends «پیامها» tab badged too. (Per-conversation unread badges already existed.) - Remove "XP گران است" / "XP is expensive" from shop.xpHint. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,12 +1,13 @@
|
||||
"use client";
|
||||
|
||||
import { ChevronLeft, ChevronRight, MessageCircle, Send } from "lucide-react";
|
||||
import { ChevronLeft, ChevronRight, MessageCircle, Send, Smile } from "lucide-react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useOnlineStore } from "@/lib/online-store";
|
||||
import { useSessionStore } from "@/lib/session-store";
|
||||
import { useUIStore } from "@/lib/ui-store";
|
||||
import { useI18n } from "@/lib/i18n";
|
||||
import { sound } from "@/lib/sound";
|
||||
import { ownedReactions } from "@/lib/online/gamification";
|
||||
import { avatarEmoji } from "@/lib/online/types";
|
||||
import { cn } from "@/lib/cn";
|
||||
|
||||
@@ -16,10 +17,13 @@ export function ChatScreen() {
|
||||
const messages = useOnlineStore((s) => s.chatMessages);
|
||||
const sendChat = useOnlineStore((s) => s.sendChat);
|
||||
const closeChat = useOnlineStore((s) => s.closeChat);
|
||||
const isPro = useSessionStore((s) => s.profile?.plan === "pro");
|
||||
const profile = useSessionStore((s) => s.profile);
|
||||
const isPro = profile?.plan === "pro";
|
||||
const navBack = useUIStore((s) => s.back);
|
||||
const viewProfile = useUIStore((s) => s.viewProfile);
|
||||
const [text, setText] = useState("");
|
||||
const [showEmoji, setShowEmoji] = useState(false);
|
||||
const emojis = profile ? ownedReactions(profile) : [];
|
||||
const endRef = useRef<HTMLDivElement>(null);
|
||||
const prevLen = useRef(0);
|
||||
const Chevron = locale === "fa" ? ChevronRight : ChevronLeft;
|
||||
@@ -49,6 +53,11 @@ export function ChatScreen() {
|
||||
await sendChat(v);
|
||||
};
|
||||
|
||||
const sendEmoji = async (e: string) => {
|
||||
setShowEmoji(false);
|
||||
await sendChat(e);
|
||||
};
|
||||
|
||||
return (
|
||||
<main className="persian-pattern relative h-dvh w-full flex justify-center">
|
||||
<div className="w-full max-w-3xl flex flex-col h-full">
|
||||
@@ -106,21 +115,45 @@ export function ChatScreen() {
|
||||
</div>
|
||||
|
||||
{/* input */}
|
||||
<footer className="glass p-3 flex items-center gap-2 shrink-0">
|
||||
<input
|
||||
value={text}
|
||||
onChange={(e) => setText(e.target.value)}
|
||||
onKeyDown={(e) => e.key === "Enter" && send()}
|
||||
placeholder={t("chat.placeholder")}
|
||||
className="flex-1 rounded-full bg-navy-900/70 gold-border px-4 py-2.5 text-cream placeholder:text-cream/30 outline-none focus:ring-2 focus:ring-gold-500/40"
|
||||
/>
|
||||
<button
|
||||
onClick={send}
|
||||
className="btn-green tap grid place-items-center rounded-full shrink-0"
|
||||
aria-label={t("chat.send")}
|
||||
>
|
||||
<Send className="size-4 rtl:-scale-x-100" />
|
||||
</button>
|
||||
<footer className="glass p-3 shrink-0 relative">
|
||||
{/* emoji / sticker picker */}
|
||||
{showEmoji && emojis.length > 0 && (
|
||||
<div className="absolute bottom-full inset-x-3 mb-2 panel rounded-2xl p-2 grid grid-cols-8 gap-1 max-h-44 overflow-y-auto">
|
||||
{emojis.map((e, i) => (
|
||||
<button
|
||||
key={i}
|
||||
onClick={() => sendEmoji(e)}
|
||||
className="text-2xl rounded-lg p-1 hover:bg-navy-800/70 active:scale-90 transition"
|
||||
>
|
||||
{e}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => setShowEmoji((v) => !v)}
|
||||
className={cn("tap grid place-items-center rounded-full shrink-0 transition", showEmoji ? "btn-gold" : "text-gold-400 hover:bg-navy-800/70")}
|
||||
aria-label={t("chat.emoji")}
|
||||
>
|
||||
<Smile className="size-5" />
|
||||
</button>
|
||||
<input
|
||||
value={text}
|
||||
onFocus={() => setShowEmoji(false)}
|
||||
onChange={(e) => setText(e.target.value)}
|
||||
onKeyDown={(e) => e.key === "Enter" && send()}
|
||||
placeholder={t("chat.placeholder")}
|
||||
className="flex-1 min-w-0 rounded-full bg-navy-900/70 gold-border px-4 py-2.5 text-cream placeholder:text-cream/30 outline-none focus:ring-2 focus:ring-gold-500/40"
|
||||
/>
|
||||
<button
|
||||
onClick={send}
|
||||
className="btn-green tap grid place-items-center rounded-full shrink-0"
|
||||
aria-label={t("chat.send")}
|
||||
>
|
||||
<Send className="size-4 rtl:-scale-x-100" />
|
||||
</button>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@@ -42,12 +42,15 @@ type Tab = "friends" | "discover" | "messages";
|
||||
export function FriendsScreen() {
|
||||
const { t } = useI18n();
|
||||
const requests = useOnlineStore((s) => s.requests);
|
||||
const unread = useOnlineStore((s) => s.unread);
|
||||
const load = useOnlineStore((s) => s.loadFriends);
|
||||
const refreshUnread = useOnlineStore((s) => s.refreshUnread);
|
||||
const [tab, setTab] = useState<Tab>("friends");
|
||||
|
||||
useEffect(() => {
|
||||
load();
|
||||
}, [load]);
|
||||
refreshUnread();
|
||||
}, [load, refreshUnread]);
|
||||
|
||||
return (
|
||||
<ScreenShell>
|
||||
@@ -57,7 +60,7 @@ export function FriendsScreen() {
|
||||
<div className="panel rounded-2xl p-1 flex gap-1 mb-4">
|
||||
<TabButton active={tab === "friends"} onClick={() => setTab("friends")} icon={<Users className="size-4" />} label={t("social.tabFriends")} badge={requests.length} />
|
||||
<TabButton active={tab === "discover"} onClick={() => setTab("discover")} icon={<Search className="size-4" />} label={t("social.tabDiscover")} />
|
||||
<TabButton active={tab === "messages"} onClick={() => setTab("messages")} icon={<MessageCircle className="size-4" />} label={t("social.tabMessages")} />
|
||||
<TabButton active={tab === "messages"} onClick={() => setTab("messages")} icon={<MessageCircle className="size-4" />} label={t("social.tabMessages")} badge={unread} />
|
||||
</div>
|
||||
|
||||
<AnimatePresence mode="wait">
|
||||
|
||||
Reference in New Issue
Block a user