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,14 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Home, ShoppingBag, Star, Trophy, User, Users } from "lucide-react";
|
import { Home, ShoppingBag, Star, Trophy, User, Users } from "lucide-react";
|
||||||
|
import { useEffect } from "react";
|
||||||
import { useUIStore, type Screen } from "@/lib/ui-store";
|
import { useUIStore, type Screen } from "@/lib/ui-store";
|
||||||
import { useSessionStore } from "@/lib/session-store";
|
import { useSessionStore } from "@/lib/session-store";
|
||||||
|
import { useOnlineStore } from "@/lib/online-store";
|
||||||
import { useI18n } from "@/lib/i18n";
|
import { useI18n } from "@/lib/i18n";
|
||||||
import { cn } from "@/lib/cn";
|
import { cn } from "@/lib/cn";
|
||||||
|
|
||||||
type Item = { key: Screen; icon: typeof Home; label: string; authed?: boolean };
|
type Item = { key: Screen; icon: typeof Home; label: string; authed?: boolean; badge?: number };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* UNO-style primary navigation. A vertical rail pinned to the side in landscape
|
* UNO-style primary navigation. A vertical rail pinned to the side in landscape
|
||||||
@@ -17,13 +19,26 @@ export function NavRail({ bottom = false }: { bottom?: boolean }) {
|
|||||||
const screen = useUIStore((s) => s.screen);
|
const screen = useUIStore((s) => s.screen);
|
||||||
const go = useUIStore((s) => s.go);
|
const go = useUIStore((s) => s.go);
|
||||||
const isAuthed = useSessionStore((s) => s.isAuthed);
|
const isAuthed = useSessionStore((s) => s.isAuthed);
|
||||||
|
const unread = useOnlineStore((s) => s.unread);
|
||||||
|
const requests = useOnlineStore((s) => s.requests);
|
||||||
|
const refreshUnread = useOnlineStore((s) => s.refreshUnread);
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
// Keep the Friends badge live (unread chats + friend requests) on every screen.
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isAuthed) return;
|
||||||
|
refreshUnread();
|
||||||
|
const id = setInterval(refreshUnread, 12000);
|
||||||
|
return () => clearInterval(id);
|
||||||
|
}, [isAuthed, refreshUnread]);
|
||||||
|
|
||||||
|
const friendsBadge = unread + requests.length;
|
||||||
|
|
||||||
const all: Item[] = [
|
const all: Item[] = [
|
||||||
{ key: "home", icon: Home, label: t("nav.home") },
|
{ key: "home", icon: Home, label: t("nav.home") },
|
||||||
{ key: "profile", icon: User, label: t("menu.profile") },
|
{ key: "profile", icon: User, label: t("menu.profile") },
|
||||||
{ key: "shop", icon: ShoppingBag, label: t("menu.shop") },
|
{ key: "shop", icon: ShoppingBag, label: t("menu.shop") },
|
||||||
{ key: "friends", icon: Users, label: t("menu.friends"), authed: true },
|
{ key: "friends", icon: Users, label: t("menu.friends"), authed: true, badge: friendsBadge },
|
||||||
{ key: "leaderboard", icon: Trophy, label: t("menu.leaderboard") },
|
{ key: "leaderboard", icon: Trophy, label: t("menu.leaderboard") },
|
||||||
{ key: "achievements", icon: Star, label: t("achv.title") },
|
{ key: "achievements", icon: Star, label: t("achv.title") },
|
||||||
];
|
];
|
||||||
@@ -52,11 +67,16 @@ export function NavRail({ bottom = false }: { bottom?: boolean }) {
|
|||||||
key={it.key}
|
key={it.key}
|
||||||
onClick={() => go(it.authed && !isAuthed ? "auth" : it.key)}
|
onClick={() => go(it.authed && !isAuthed ? "auth" : it.key)}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex min-w-[54px] flex-col items-center justify-center gap-1 rounded-2xl px-1.5 py-2 transition",
|
"relative flex min-w-[54px] flex-col items-center justify-center gap-1 rounded-2xl px-1.5 py-2 transition",
|
||||||
!bottom && "landscape:w-full landscape:py-2.5",
|
!bottom && "landscape:w-full landscape:py-2.5",
|
||||||
active ? "btn-gold shadow-lg" : "text-cream/55 hover:bg-navy-800/60 hover:text-cream"
|
active ? "btn-gold shadow-lg" : "text-cream/55 hover:bg-navy-800/60 hover:text-cream"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
{!!it.badge && it.badge > 0 && (
|
||||||
|
<span className="absolute top-1 ltr:right-2 rtl:left-2 min-w-4 h-4 px-1 rounded-full bg-rose-500 text-[9px] font-bold text-white grid place-items-center">
|
||||||
|
{it.badge > 9 ? "9+" : it.badge}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
<it.icon className={cn("size-5", active && "text-[#2a1f04]")} />
|
<it.icon className={cn("size-5", active && "text-[#2a1f04]")} />
|
||||||
<span className={cn("text-[10px] font-bold leading-none", active && "text-[#2a1f04]")}>
|
<span className={cn("text-[10px] font-bold leading-none", active && "text-[#2a1f04]")}>
|
||||||
{it.label}
|
{it.label}
|
||||||
|
|||||||
@@ -77,13 +77,13 @@ export function PostMatchRewardsModal({
|
|||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0 }}
|
initial={{ opacity: 0 }}
|
||||||
animate={{ opacity: 1 }}
|
animate={{ opacity: 1 }}
|
||||||
className="fixed inset-0 z-50 flex items-center justify-center bg-navy-950/85 backdrop-blur-sm p-5"
|
className="fixed inset-0 z-50 flex items-center justify-center bg-navy-950/85 backdrop-blur-sm p-3 sm:p-5"
|
||||||
>
|
>
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ scale: 0.82, y: 28 }}
|
initial={{ scale: 0.82, y: 28 }}
|
||||||
animate={{ scale: 1, y: 0 }}
|
animate={{ scale: 1, y: 0 }}
|
||||||
transition={{ type: "spring", stiffness: 200, damping: 18 }}
|
transition={{ type: "spring", stiffness: 200, damping: 18 }}
|
||||||
className="glass rounded-3xl p-7 w-full max-w-sm text-center relative max-h-[88dvh] overflow-y-auto overflow-x-hidden"
|
className="glass rounded-3xl p-7 short:p-4 w-full max-w-sm landscape:max-w-md text-center relative max-h-[94dvh] overflow-y-auto overflow-x-hidden"
|
||||||
>
|
>
|
||||||
{/* radiating bg glow */}
|
{/* radiating bg glow */}
|
||||||
<div
|
<div
|
||||||
@@ -98,13 +98,13 @@ export function PostMatchRewardsModal({
|
|||||||
initial={{ scale: 0, rotate: -20 }}
|
initial={{ scale: 0, rotate: -20 }}
|
||||||
animate={{ scale: 1, rotate: 0 }}
|
animate={{ scale: 1, rotate: 0 }}
|
||||||
transition={{ type: "spring", stiffness: 180, delay: 0.1 }}
|
transition={{ type: "spring", stiffness: 180, delay: 0.1 }}
|
||||||
className="text-6xl mb-2 relative"
|
className="text-6xl short:text-4xl mb-2 short:mb-0 relative"
|
||||||
>
|
>
|
||||||
{won ? "🏆" : "🎴"}
|
{won ? "🏆" : "🎴"}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
<h2 className="gold-text text-2xl font-black relative">{t("reward.title")}</h2>
|
<h2 className="gold-text text-2xl short:text-xl font-black relative">{t("reward.title")}</h2>
|
||||||
<p className={"relative mt-1 font-bold text-lg " + (won ? "text-teal-300" : "text-rose-300")}>
|
<p className={"relative mt-1 font-bold text-lg short:text-base " + (won ? "text-teal-300" : "text-rose-300")}>
|
||||||
{won ? t("reward.win") : t("reward.lose")}
|
{won ? t("reward.win") : t("reward.lose")}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@@ -114,9 +114,9 @@ export function PostMatchRewardsModal({
|
|||||||
initial={{ scale: 0.5, opacity: 0 }}
|
initial={{ scale: 0.5, opacity: 0 }}
|
||||||
animate={{ scale: 1, opacity: 1 }}
|
animate={{ scale: 1, opacity: 1 }}
|
||||||
transition={{ type: "spring", stiffness: 200, damping: 14, delay: 0.18 }}
|
transition={{ type: "spring", stiffness: 200, damping: 14, delay: 0.18 }}
|
||||||
className="relative mt-4 flex items-center justify-center gap-2"
|
className="relative mt-4 short:mt-2 flex items-center justify-center gap-2"
|
||||||
>
|
>
|
||||||
<span className="text-5xl font-black gold-text">
|
<span className="text-5xl short:text-3xl font-black gold-text">
|
||||||
+<CountUp to={reward.coinsDelta} ms={1100} />
|
+<CountUp to={reward.coinsDelta} ms={1100} />
|
||||||
</span>
|
</span>
|
||||||
<motion.span
|
<motion.span
|
||||||
@@ -128,7 +128,7 @@ export function PostMatchRewardsModal({
|
|||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="relative mt-5 space-y-2">
|
<div className="relative mt-5 short:mt-3 space-y-2 short:space-y-1.5">
|
||||||
{reward.ratingDelta !== 0 && (
|
{reward.ratingDelta !== 0 && (
|
||||||
<RewardRow
|
<RewardRow
|
||||||
icon={reward.ratingDelta > 0
|
icon={reward.ratingDelta > 0
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
"use client";
|
"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 { useEffect, useRef, useState } from "react";
|
||||||
import { useOnlineStore } from "@/lib/online-store";
|
import { useOnlineStore } from "@/lib/online-store";
|
||||||
import { useSessionStore } from "@/lib/session-store";
|
import { useSessionStore } from "@/lib/session-store";
|
||||||
import { useUIStore } from "@/lib/ui-store";
|
import { useUIStore } from "@/lib/ui-store";
|
||||||
import { useI18n } from "@/lib/i18n";
|
import { useI18n } from "@/lib/i18n";
|
||||||
import { sound } from "@/lib/sound";
|
import { sound } from "@/lib/sound";
|
||||||
|
import { ownedReactions } from "@/lib/online/gamification";
|
||||||
import { avatarEmoji } from "@/lib/online/types";
|
import { avatarEmoji } from "@/lib/online/types";
|
||||||
import { cn } from "@/lib/cn";
|
import { cn } from "@/lib/cn";
|
||||||
|
|
||||||
@@ -16,10 +17,13 @@ export function ChatScreen() {
|
|||||||
const messages = useOnlineStore((s) => s.chatMessages);
|
const messages = useOnlineStore((s) => s.chatMessages);
|
||||||
const sendChat = useOnlineStore((s) => s.sendChat);
|
const sendChat = useOnlineStore((s) => s.sendChat);
|
||||||
const closeChat = useOnlineStore((s) => s.closeChat);
|
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 navBack = useUIStore((s) => s.back);
|
||||||
const viewProfile = useUIStore((s) => s.viewProfile);
|
const viewProfile = useUIStore((s) => s.viewProfile);
|
||||||
const [text, setText] = useState("");
|
const [text, setText] = useState("");
|
||||||
|
const [showEmoji, setShowEmoji] = useState(false);
|
||||||
|
const emojis = profile ? ownedReactions(profile) : [];
|
||||||
const endRef = useRef<HTMLDivElement>(null);
|
const endRef = useRef<HTMLDivElement>(null);
|
||||||
const prevLen = useRef(0);
|
const prevLen = useRef(0);
|
||||||
const Chevron = locale === "fa" ? ChevronRight : ChevronLeft;
|
const Chevron = locale === "fa" ? ChevronRight : ChevronLeft;
|
||||||
@@ -49,6 +53,11 @@ export function ChatScreen() {
|
|||||||
await sendChat(v);
|
await sendChat(v);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const sendEmoji = async (e: string) => {
|
||||||
|
setShowEmoji(false);
|
||||||
|
await sendChat(e);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="persian-pattern relative h-dvh w-full flex justify-center">
|
<main className="persian-pattern relative h-dvh w-full flex justify-center">
|
||||||
<div className="w-full max-w-3xl flex flex-col h-full">
|
<div className="w-full max-w-3xl flex flex-col h-full">
|
||||||
@@ -106,13 +115,36 @@ export function ChatScreen() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* input */}
|
{/* input */}
|
||||||
<footer className="glass p-3 flex items-center gap-2 shrink-0">
|
<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
|
<input
|
||||||
value={text}
|
value={text}
|
||||||
|
onFocus={() => setShowEmoji(false)}
|
||||||
onChange={(e) => setText(e.target.value)}
|
onChange={(e) => setText(e.target.value)}
|
||||||
onKeyDown={(e) => e.key === "Enter" && send()}
|
onKeyDown={(e) => e.key === "Enter" && send()}
|
||||||
placeholder={t("chat.placeholder")}
|
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"
|
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
|
<button
|
||||||
onClick={send}
|
onClick={send}
|
||||||
@@ -121,6 +153,7 @@ export function ChatScreen() {
|
|||||||
>
|
>
|
||||||
<Send className="size-4 rtl:-scale-x-100" />
|
<Send className="size-4 rtl:-scale-x-100" />
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -42,12 +42,15 @@ type Tab = "friends" | "discover" | "messages";
|
|||||||
export function FriendsScreen() {
|
export function FriendsScreen() {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const requests = useOnlineStore((s) => s.requests);
|
const requests = useOnlineStore((s) => s.requests);
|
||||||
|
const unread = useOnlineStore((s) => s.unread);
|
||||||
const load = useOnlineStore((s) => s.loadFriends);
|
const load = useOnlineStore((s) => s.loadFriends);
|
||||||
|
const refreshUnread = useOnlineStore((s) => s.refreshUnread);
|
||||||
const [tab, setTab] = useState<Tab>("friends");
|
const [tab, setTab] = useState<Tab>("friends");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
load();
|
load();
|
||||||
}, [load]);
|
refreshUnread();
|
||||||
|
}, [load, refreshUnread]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScreenShell>
|
<ScreenShell>
|
||||||
@@ -57,7 +60,7 @@ export function FriendsScreen() {
|
|||||||
<div className="panel rounded-2xl p-1 flex gap-1 mb-4">
|
<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 === "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 === "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>
|
</div>
|
||||||
|
|
||||||
<AnimatePresence mode="wait">
|
<AnimatePresence mode="wait">
|
||||||
|
|||||||
+4
-2
@@ -151,6 +151,7 @@ const fa: Dict = {
|
|||||||
"chat.title": "گفتگو",
|
"chat.title": "گفتگو",
|
||||||
"chat.placeholder": "پیام بنویسید…",
|
"chat.placeholder": "پیام بنویسید…",
|
||||||
"chat.send": "ارسال",
|
"chat.send": "ارسال",
|
||||||
|
"chat.emoji": "ایموجی",
|
||||||
"chat.empty": "گفتگو را شروع کنید",
|
"chat.empty": "گفتگو را شروع کنید",
|
||||||
"friends.message": "پیام",
|
"friends.message": "پیام",
|
||||||
|
|
||||||
@@ -315,7 +316,7 @@ const fa: Dict = {
|
|||||||
"shop.titles": "عناوین",
|
"shop.titles": "عناوین",
|
||||||
"shop.titlesHint": "عنوان شما زیر نامتان در بازی و لیستها نمایش داده میشود",
|
"shop.titlesHint": "عنوان شما زیر نامتان در بازی و لیستها نمایش داده میشود",
|
||||||
"shop.xp": "امتیاز تجربه (XP)",
|
"shop.xp": "امتیاز تجربه (XP)",
|
||||||
"shop.xpHint": "افزایش سریع سطح — XP گران است",
|
"shop.xpHint": "افزایش سریع سطح",
|
||||||
"shop.includes": "شامل",
|
"shop.includes": "شامل",
|
||||||
"shop.reqLevel": "سطح",
|
"shop.reqLevel": "سطح",
|
||||||
"shop.reqRating": "امتیاز",
|
"shop.reqRating": "امتیاز",
|
||||||
@@ -501,6 +502,7 @@ const en: Dict = {
|
|||||||
"chat.title": "Chat",
|
"chat.title": "Chat",
|
||||||
"chat.placeholder": "Type a message…",
|
"chat.placeholder": "Type a message…",
|
||||||
"chat.send": "Send",
|
"chat.send": "Send",
|
||||||
|
"chat.emoji": "Emoji",
|
||||||
"chat.empty": "Start the conversation",
|
"chat.empty": "Start the conversation",
|
||||||
"friends.message": "Message",
|
"friends.message": "Message",
|
||||||
|
|
||||||
@@ -662,7 +664,7 @@ const en: Dict = {
|
|||||||
"shop.titles": "Titles",
|
"shop.titles": "Titles",
|
||||||
"shop.titlesHint": "Your title shows under your name in games & lists",
|
"shop.titlesHint": "Your title shows under your name in games & lists",
|
||||||
"shop.xp": "XP packs",
|
"shop.xp": "XP packs",
|
||||||
"shop.xpHint": "Level up faster — XP is expensive",
|
"shop.xpHint": "Level up faster",
|
||||||
"shop.includes": "Includes",
|
"shop.includes": "Includes",
|
||||||
"shop.reqLevel": "Level",
|
"shop.reqLevel": "Level",
|
||||||
"shop.reqRating": "Rating",
|
"shop.reqRating": "Rating",
|
||||||
|
|||||||
@@ -41,9 +41,11 @@ interface OnlineStore {
|
|||||||
// chat
|
// chat
|
||||||
activeChatFriend: Friend | null;
|
activeChatFriend: Friend | null;
|
||||||
chatMessages: ChatMessage[];
|
chatMessages: ChatMessage[];
|
||||||
|
unread: number; // total unread messages across conversations (for nav badge)
|
||||||
openChat: (friend: Friend) => Promise<void>;
|
openChat: (friend: Friend) => Promise<void>;
|
||||||
sendChat: (text: string) => Promise<void>;
|
sendChat: (text: string) => Promise<void>;
|
||||||
closeChat: () => void;
|
closeChat: () => void;
|
||||||
|
refreshUnread: () => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
let roomUnsub: (() => void) | null = null;
|
let roomUnsub: (() => void) | null = null;
|
||||||
@@ -154,11 +156,22 @@ export const useOnlineStore = create<OnlineStore>((set, get) => ({
|
|||||||
|
|
||||||
activeChatFriend: null,
|
activeChatFriend: null,
|
||||||
chatMessages: [],
|
chatMessages: [],
|
||||||
|
unread: 0,
|
||||||
|
|
||||||
|
refreshUnread: async () => {
|
||||||
|
try {
|
||||||
|
const convs = await getService().listConversations();
|
||||||
|
set({ unread: convs.reduce((n, c) => n + (c.unread ?? 0), 0) });
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
openChat: async (friend) => {
|
openChat: async (friend) => {
|
||||||
const svc = getService();
|
const svc = getService();
|
||||||
set({ activeChatFriend: friend, chatMessages: await svc.getMessages(friend.id) });
|
set({ activeChatFriend: friend, chatMessages: await svc.getMessages(friend.id) });
|
||||||
await svc.markRead(friend.id);
|
await svc.markRead(friend.id);
|
||||||
|
get().refreshUnread();
|
||||||
if (chatUnsub) chatUnsub();
|
if (chatUnsub) chatUnsub();
|
||||||
chatUnsub = svc.onChat((friendId, msgs) => {
|
chatUnsub = svc.onChat((friendId, msgs) => {
|
||||||
const active = get().activeChatFriend;
|
const active = get().activeChatFriend;
|
||||||
@@ -182,5 +195,6 @@ export const useOnlineStore = create<OnlineStore>((set, get) => ({
|
|||||||
chatUnsub = null;
|
chatUnsub = null;
|
||||||
}
|
}
|
||||||
set({ activeChatFriend: null, chatMessages: [] });
|
set({ activeChatFriend: null, chatMessages: [] });
|
||||||
|
get().refreshUnread();
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|||||||
Reference in New Issue
Block a user