lang
This commit is contained in:
@@ -1,11 +1,34 @@
|
||||
import type { ChatMessage } from "@/components/chat/ChatMessageCard";
|
||||
|
||||
export function parseMessageTime(message: ChatMessage): number {
|
||||
const withIso = message as ChatMessage & { createdAtIso?: string };
|
||||
if (withIso.createdAtIso) {
|
||||
const t = new Date(withIso.createdAtIso).getTime();
|
||||
if (!Number.isNaN(t)) return t;
|
||||
}
|
||||
if (message.createdAt?.includes("T")) {
|
||||
const t = new Date(message.createdAt).getTime();
|
||||
if (!Number.isNaN(t)) return t;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
const PENDING_MATCH_WINDOW_MS = 120_000;
|
||||
|
||||
function matchesPendingServer(server: ChatMessage, pending: ChatMessage): boolean {
|
||||
if (String(server.senderId) !== String(pending.senderId)) return false;
|
||||
if (pending.fileType !== server.fileType) return false;
|
||||
if (String(pending.fileType || "") !== String(server.fileType || "")) return false;
|
||||
|
||||
const pendingTime = parseMessageTime(pending);
|
||||
const serverTime = parseMessageTime(server);
|
||||
if (pendingTime && serverTime) {
|
||||
if (Math.abs(serverTime - pendingTime) > PENDING_MATCH_WINDOW_MS) return false;
|
||||
}
|
||||
|
||||
if (pending.fileType === "location" || (!pending.fileType && !pending.file)) {
|
||||
return server.content === pending.content;
|
||||
}
|
||||
|
||||
return Boolean(pending.fileType && server.fileType === pending.fileType);
|
||||
}
|
||||
|
||||
@@ -20,10 +43,10 @@ export function filterResolvedPendingMessages(
|
||||
server: ChatMessage[],
|
||||
pending: ChatMessage[]
|
||||
): ChatMessage[] {
|
||||
return pending.filter(
|
||||
(p) =>
|
||||
!server.some((s) => s._id === p._id || matchesPendingServer(s, p))
|
||||
);
|
||||
return pending.filter((p) => {
|
||||
if (server.some((s) => String(s._id) === String(p._id))) return false;
|
||||
return !server.some((s) => matchesPendingServer(s, p));
|
||||
});
|
||||
}
|
||||
|
||||
export function dedupeChatMessages(messages: ChatMessage[]): ChatMessage[] {
|
||||
|
||||
@@ -1,9 +1,63 @@
|
||||
/** Parse message timestamp (ISO or datetime string) for grouping */
|
||||
import { DEFAULT_LANGUAGE, type AppLanguage } from "@/lib/i18n/registry";
|
||||
import { translateCommon } from "@/lib/i18n/translate";
|
||||
|
||||
function normalizeJalaliPrefix(raw: string): string | null {
|
||||
const match = raw.match(/^(\d{4})-(\d{1,2})-(\d{1,2})/);
|
||||
if (!match) return null;
|
||||
return `${match[1]}-${Number(match[2])}-${Number(match[3])}`;
|
||||
}
|
||||
|
||||
function getTodayJalaliPrefix(): string | null {
|
||||
try {
|
||||
const parts = new Intl.DateTimeFormat("en-u-ca-persian", {
|
||||
year: "numeric",
|
||||
month: "numeric",
|
||||
day: "numeric",
|
||||
}).formatToParts(new Date());
|
||||
|
||||
const year = parts.find((p) => p.type === "year")?.value;
|
||||
const month = parts.find((p) => p.type === "month")?.value;
|
||||
const day = parts.find((p) => p.type === "day")?.value;
|
||||
if (!year || !month || !day) return null;
|
||||
return `${year}-${Number(month)}-${Number(day)}`;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function getYesterdayJalaliPrefix(): string | null {
|
||||
const yesterday = new Date();
|
||||
yesterday.setDate(yesterday.getDate() - 1);
|
||||
try {
|
||||
const parts = new Intl.DateTimeFormat("en-u-ca-persian", {
|
||||
year: "numeric",
|
||||
month: "numeric",
|
||||
day: "numeric",
|
||||
}).formatToParts(yesterday);
|
||||
|
||||
const year = parts.find((p) => p.type === "year")?.value;
|
||||
const month = parts.find((p) => p.type === "month")?.value;
|
||||
const day = parts.find((p) => p.type === "day")?.value;
|
||||
if (!year || !month || !day) return null;
|
||||
return `${year}-${Number(month)}-${Number(day)}`;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Parse message timestamp (ISO, Jalali, or datetime string) for grouping */
|
||||
export function parseMessageDate(createdAt?: string | null): Date | null {
|
||||
if (!createdAt) return null;
|
||||
const raw = String(createdAt).trim();
|
||||
const d = new Date(raw);
|
||||
if (!Number.isNaN(d.getTime())) return d;
|
||||
|
||||
const jalaliPrefix = normalizeJalaliPrefix(raw);
|
||||
if (jalaliPrefix) {
|
||||
const [y, m, d] = jalaliPrefix.split("-").map(Number);
|
||||
return new Date(y, m - 1, d);
|
||||
}
|
||||
|
||||
const parsed = new Date(raw);
|
||||
if (!Number.isNaN(parsed.getTime())) return parsed;
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -11,40 +65,98 @@ function isSameDay(a: Date, b: Date): boolean {
|
||||
return a.toDateString() === b.toDateString();
|
||||
}
|
||||
|
||||
/** Telegram-style date label: امروز، دیروز، or calendar date */
|
||||
export function formatChatDateLabel(createdAt?: string | null): string {
|
||||
function formatLocalizedDate(
|
||||
date: Date,
|
||||
withYear: boolean,
|
||||
lang: AppLanguage
|
||||
): string {
|
||||
const locale = lang === "fa" ? "fa-IR" : "en-US";
|
||||
try {
|
||||
if (withYear) {
|
||||
return date.toLocaleDateString(locale, {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
});
|
||||
}
|
||||
return date.toLocaleDateString(locale, {
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
});
|
||||
} catch {
|
||||
return date.toDateString();
|
||||
}
|
||||
}
|
||||
|
||||
/** Telegram-style date label: today, yesterday, or calendar date */
|
||||
export function formatChatDateLabel(
|
||||
createdAt?: string | null,
|
||||
lang: AppLanguage = DEFAULT_LANGUAGE
|
||||
): string {
|
||||
const raw = createdAt ? String(createdAt).trim() : "";
|
||||
const jalaliPrefix = normalizeJalaliPrefix(raw);
|
||||
|
||||
if (jalaliPrefix) {
|
||||
const today = getTodayJalaliPrefix();
|
||||
const yesterday = getYesterdayJalaliPrefix();
|
||||
if (today && jalaliPrefix === today) {
|
||||
return translateCommon(lang, "chats.today");
|
||||
}
|
||||
if (yesterday && jalaliPrefix === yesterday) {
|
||||
return translateCommon(lang, "chats.yesterday");
|
||||
}
|
||||
|
||||
const [y, m, d] = jalaliPrefix.split("-").map(Number);
|
||||
const date = new Date(y, m - 1, d);
|
||||
if (!Number.isNaN(date.getTime())) {
|
||||
return formatLocalizedDate(date, y !== new Date().getFullYear(), lang);
|
||||
}
|
||||
return jalaliPrefix.replace(/-/g, "/");
|
||||
}
|
||||
|
||||
const d = parseMessageDate(createdAt) ?? new Date();
|
||||
const today = new Date();
|
||||
const yesterday = new Date();
|
||||
yesterday.setDate(today.getDate() - 1);
|
||||
|
||||
if (isSameDay(d, today)) return "امروز";
|
||||
if (isSameDay(d, yesterday)) return "دیروز";
|
||||
if (isSameDay(d, today)) return translateCommon(lang, "chats.today");
|
||||
if (isSameDay(d, yesterday)) return translateCommon(lang, "chats.yesterday");
|
||||
|
||||
if (d.getFullYear() === today.getFullYear()) {
|
||||
return d.toLocaleDateString("fa-IR", { month: "long", day: "numeric" });
|
||||
return formatLocalizedDate(d, false, lang);
|
||||
}
|
||||
|
||||
return d.toLocaleDateString("fa-IR", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
});
|
||||
return formatLocalizedDate(d, true, lang);
|
||||
}
|
||||
|
||||
/** Bubble time: hours, minutes, seconds only (no date) */
|
||||
export function formatBubbleTime(createdAt?: string | null): string {
|
||||
export function formatBubbleTime(
|
||||
createdAt?: string | null,
|
||||
lang: AppLanguage = DEFAULT_LANGUAGE
|
||||
): string {
|
||||
if (!createdAt) {
|
||||
return new Date().toLocaleTimeString("fa-IR", {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
});
|
||||
return "";
|
||||
}
|
||||
|
||||
const raw = String(createdAt).trim();
|
||||
const locale = lang === "fa" ? "fa-IR" : "en-US";
|
||||
|
||||
// ISO datetime — always prefer real clock time
|
||||
if (raw.includes("T")) {
|
||||
const d = new Date(raw);
|
||||
if (!Number.isNaN(d.getTime())) {
|
||||
try {
|
||||
return d.toLocaleTimeString(locale, {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
});
|
||||
} catch {
|
||||
return raw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Jalali datetime "1403-01-15 14:30:45" or ISO
|
||||
const timePart = raw.includes(" ")
|
||||
? raw.split(" ").pop()
|
||||
: raw.length <= 8 && raw.includes(":")
|
||||
@@ -57,13 +169,17 @@ export function formatBubbleTime(createdAt?: string | null): string {
|
||||
return timePart;
|
||||
}
|
||||
|
||||
const d = new Date(raw);
|
||||
const d = parseMessageDate(raw) ?? new Date(raw);
|
||||
if (!Number.isNaN(d.getTime())) {
|
||||
return d.toLocaleTimeString("fa-IR", {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
});
|
||||
try {
|
||||
return d.toLocaleTimeString(locale, {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
});
|
||||
} catch {
|
||||
return raw;
|
||||
}
|
||||
}
|
||||
|
||||
return raw;
|
||||
|
||||
111
src/lib/chat/getCopyableMessageText.ts
Normal file
111
src/lib/chat/getCopyableMessageText.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import { parseLocationContent } from "@/components/chat/LocationMessageBubble";
|
||||
import { parseSharedPost } from "@/components/chat/SharedPostBubble";
|
||||
import {
|
||||
extractStoryReactionText,
|
||||
parseSharedStory,
|
||||
} from "@/components/chat/SharedStoryBubble";
|
||||
import type { ChatMessage } from "@/components/chat/ChatMessageCard";
|
||||
|
||||
function parseForwarded(content: string): ChatMessage["forwardedFrom"] | null {
|
||||
try {
|
||||
const line = content.split("\n")[0];
|
||||
const parsed = JSON.parse(line);
|
||||
return parsed.forwardedFrom ?? null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function stripJsonPrefix(content: string): string {
|
||||
return content.replace(/^\{.*\}\n?/, "").trim();
|
||||
}
|
||||
|
||||
function getTextAfterFirstLine(content: string): string {
|
||||
const lines = content.split("\n");
|
||||
if (lines.length <= 1) return "";
|
||||
return lines.slice(1).join("\n").trim();
|
||||
}
|
||||
|
||||
type CopyableMessage = {
|
||||
content?: string;
|
||||
file?: string;
|
||||
fileType?: ChatMessage["fileType"];
|
||||
forwardedFrom?: ChatMessage["forwardedFrom"];
|
||||
};
|
||||
|
||||
export function getCopyableMessageText(
|
||||
message: CopyableMessage
|
||||
): string | null {
|
||||
const content = message.content?.trim() || "";
|
||||
if (!content && !message.file) return null;
|
||||
|
||||
const location =
|
||||
message.fileType === "location" ? parseLocationContent(content) : null;
|
||||
if (location) {
|
||||
const label = location.label?.trim();
|
||||
const coords = `${location.lat}, ${location.lng}`;
|
||||
return label ? `${label}\n${coords}` : coords;
|
||||
}
|
||||
|
||||
const sharedPost = parseSharedPost(content);
|
||||
if (sharedPost) {
|
||||
const caption = sharedPost.caption?.trim();
|
||||
if (caption) return caption;
|
||||
return null;
|
||||
}
|
||||
|
||||
const sharedStory = parseSharedStory(content);
|
||||
if (sharedStory) {
|
||||
const reactionText = extractStoryReactionText(content);
|
||||
return reactionText || null;
|
||||
}
|
||||
|
||||
const forwarded = message.forwardedFrom || parseForwarded(content);
|
||||
if (forwarded) {
|
||||
return getTextAfterFirstLine(content) || null;
|
||||
}
|
||||
|
||||
if (parseLocationContent(content)) {
|
||||
const loc = parseLocationContent(content)!;
|
||||
const label = loc.label?.trim();
|
||||
const coords = `${loc.lat}, ${loc.lng}`;
|
||||
return label ? `${label}\n${coords}` : coords;
|
||||
}
|
||||
|
||||
const text = stripJsonPrefix(content);
|
||||
return text || null;
|
||||
}
|
||||
|
||||
export function getCopyableRoomMessageText(content?: string): string | null {
|
||||
const text = content?.trim();
|
||||
return text || null;
|
||||
}
|
||||
|
||||
export async function copyTextToClipboard(text: string): Promise<boolean> {
|
||||
if (typeof window === "undefined") return false;
|
||||
|
||||
try {
|
||||
if (navigator.clipboard?.writeText) {
|
||||
await navigator.clipboard.writeText(text);
|
||||
return true;
|
||||
}
|
||||
} catch {
|
||||
// fallback below
|
||||
}
|
||||
|
||||
try {
|
||||
const textarea = document.createElement("textarea");
|
||||
textarea.value = text;
|
||||
textarea.setAttribute("readonly", "");
|
||||
textarea.style.position = "fixed";
|
||||
textarea.style.opacity = "0";
|
||||
textarea.style.pointerEvents = "none";
|
||||
document.body.appendChild(textarea);
|
||||
textarea.select();
|
||||
const copied = document.execCommand("copy");
|
||||
document.body.removeChild(textarea);
|
||||
return copied;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
formatChatDateLabel,
|
||||
parseMessageDate,
|
||||
} from "@/lib/chat/formatMessageTime";
|
||||
import { DEFAULT_LANGUAGE, type AppLanguage } from "@/lib/i18n/registry";
|
||||
|
||||
export type MessageGroup =
|
||||
| { type: "date"; label: string; key: string }
|
||||
@@ -14,7 +15,10 @@ function dayKey(msg: ChatMessage): string {
|
||||
return "unknown-day";
|
||||
}
|
||||
|
||||
export function groupMessagesByDate(messages: ChatMessage[]): MessageGroup[] {
|
||||
export function groupMessagesByDate(
|
||||
messages: ChatMessage[],
|
||||
lang: AppLanguage = DEFAULT_LANGUAGE
|
||||
): MessageGroup[] {
|
||||
const groups: MessageGroup[] = [];
|
||||
let lastDay = "";
|
||||
|
||||
@@ -26,7 +30,7 @@ export function groupMessagesByDate(messages: ChatMessage[]): MessageGroup[] {
|
||||
lastDay = dk;
|
||||
groups.push({
|
||||
type: "date",
|
||||
label: formatChatDateLabel(msg.createdAtIso ?? msg.createdAt),
|
||||
label: formatChatDateLabel(msg.createdAtIso ?? msg.createdAt, lang),
|
||||
key: `date-${dk}`,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -78,3 +78,8 @@ export function getMessageRowSpacing(
|
||||
if (isGroupedWithPrev) return "mt-[2px]";
|
||||
return "mt-3";
|
||||
}
|
||||
|
||||
/** Media bubbles always use fully rounded corners (photo/video/file preview). */
|
||||
export function getMediaBubbleRadius(): string {
|
||||
return "18px";
|
||||
}
|
||||
|
||||
20
src/lib/chat/onlineStatus.ts
Normal file
20
src/lib/chat/onlineStatus.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
/** Legacy API values returned before i18n rollout */
|
||||
const LEGACY_ONLINE_VALUES = new Set(["آنلاین", "Online"]);
|
||||
|
||||
export function isUserOnline(
|
||||
lastOnline?: string | null,
|
||||
onlineLabel?: string
|
||||
): boolean {
|
||||
if (!lastOnline) return false;
|
||||
if (onlineLabel && lastOnline === onlineLabel) return true;
|
||||
return LEGACY_ONLINE_VALUES.has(lastOnline);
|
||||
}
|
||||
|
||||
export function resolveOnlineLabel(
|
||||
lastOnline: string | undefined,
|
||||
onlineLabel: string,
|
||||
offlineLabel: string
|
||||
): string {
|
||||
if (isUserOnline(lastOnline, onlineLabel)) return onlineLabel;
|
||||
return lastOnline || offlineLabel;
|
||||
}
|
||||
15
src/lib/chat/reactions.ts
Normal file
15
src/lib/chat/reactions.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
export const QUICK_REACTIONS = ["❤️", "😂", "👍", "😮", "😢", "🙏"] as const;
|
||||
|
||||
export type MessageReaction = {
|
||||
userId: string;
|
||||
emoji: string;
|
||||
};
|
||||
|
||||
export function groupReactions(reactions: MessageReaction[] | undefined) {
|
||||
if (!reactions?.length) return [];
|
||||
const map = new Map<string, number>();
|
||||
for (const r of reactions) {
|
||||
map.set(r.emoji, (map.get(r.emoji) ?? 0) + 1);
|
||||
}
|
||||
return [...map.entries()].map(([emoji, count]) => ({ emoji, count }));
|
||||
}
|
||||
@@ -3,10 +3,36 @@ import { SOCKET_URL } from "@/components/main/BaseUrl";
|
||||
|
||||
let sharedSocket: Socket | null = null;
|
||||
let refCount = 0;
|
||||
const connectionListeners = new Set<(connected: boolean) => void>();
|
||||
|
||||
type PendingGroupJoin = { roomId: string; userId: string };
|
||||
const pendingGroupJoins = new Map<string, PendingGroupJoin>();
|
||||
let pendingUserId: string | null = null;
|
||||
|
||||
function notifyConnectionListeners(connected: boolean) {
|
||||
connectionListeners.forEach((fn) => fn(connected));
|
||||
}
|
||||
|
||||
function flushPendingJoins(): void {
|
||||
if (!sharedSocket?.connected) return;
|
||||
|
||||
if (pendingUserId) {
|
||||
sharedSocket.emit("joinUser", { userId: pendingUserId });
|
||||
}
|
||||
|
||||
for (const { roomId, userId } of pendingGroupJoins.values()) {
|
||||
sharedSocket.emit("joinGroupRoom", { roomId, userId });
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveSocketUrl(): string {
|
||||
return process.env.NEXT_PUBLIC_SOCKET_URL || SOCKET_URL;
|
||||
}
|
||||
|
||||
export function acquireChatSocket(): Socket {
|
||||
if (!sharedSocket) {
|
||||
sharedSocket = io(SOCKET_URL, {
|
||||
sharedSocket = io(resolveSocketUrl(), {
|
||||
path: "/socket.io/",
|
||||
transports: ["websocket", "polling"],
|
||||
reconnection: true,
|
||||
reconnectionAttempts: Infinity,
|
||||
@@ -16,6 +42,13 @@ export function acquireChatSocket(): Socket {
|
||||
withCredentials: true,
|
||||
autoConnect: true,
|
||||
});
|
||||
|
||||
sharedSocket.on("connect", () => {
|
||||
notifyConnectionListeners(true);
|
||||
flushPendingJoins();
|
||||
});
|
||||
sharedSocket.on("disconnect", () => notifyConnectionListeners(false));
|
||||
sharedSocket.on("connect_error", () => notifyConnectionListeners(false));
|
||||
}
|
||||
refCount += 1;
|
||||
return sharedSocket;
|
||||
@@ -35,13 +68,19 @@ export function disconnectChatSocket(): void {
|
||||
sharedSocket.disconnect();
|
||||
sharedSocket = null;
|
||||
refCount = 0;
|
||||
pendingGroupJoins.clear();
|
||||
pendingUserId = null;
|
||||
notifyConnectionListeners(false);
|
||||
}
|
||||
}
|
||||
|
||||
export function joinUserRoom(userId: string): void {
|
||||
const id = String(userId).trim();
|
||||
if (!id || !sharedSocket) return;
|
||||
sharedSocket.emit("joinUser", { userId: id });
|
||||
if (!id) return;
|
||||
pendingUserId = id;
|
||||
if (sharedSocket?.connected) {
|
||||
sharedSocket.emit("joinUser", { userId: id });
|
||||
}
|
||||
}
|
||||
|
||||
export function joinChatRoom(userId: string, receiverId: string): void {
|
||||
@@ -51,6 +90,30 @@ export function joinChatRoom(userId: string, receiverId: string): void {
|
||||
sharedSocket.emit("joinChat", { userId: uid, receiverId: rid });
|
||||
}
|
||||
|
||||
export function joinGroupRoom(roomId: string, userId: string): void {
|
||||
const rid = String(roomId).trim();
|
||||
const uid = String(userId).trim();
|
||||
if (!rid || !uid) return;
|
||||
pendingGroupJoins.set(rid, { roomId: rid, userId: uid });
|
||||
if (sharedSocket?.connected) {
|
||||
sharedSocket.emit("joinGroupRoom", { roomId: rid, userId: uid });
|
||||
}
|
||||
}
|
||||
|
||||
export function leaveGroupRoom(roomId: string): void {
|
||||
pendingGroupJoins.delete(String(roomId).trim());
|
||||
}
|
||||
|
||||
export function isChatSocketConnected(): boolean {
|
||||
return Boolean(sharedSocket?.connected);
|
||||
}
|
||||
|
||||
export function subscribeChatSocketConnection(
|
||||
listener: (connected: boolean) => void
|
||||
): () => void {
|
||||
connectionListeners.add(listener);
|
||||
listener(isChatSocketConnected());
|
||||
return () => {
|
||||
connectionListeners.delete(listener);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { InfiniteData } from "@tanstack/react-query";
|
||||
import type { ChatMessage } from "@/components/chat/ChatMessageCard";
|
||||
import { parseMessageTime } from "./dedupeMessages";
|
||||
|
||||
export type ChatThreadPage = { messages: ChatMessage[]; nextPage?: number };
|
||||
export type ChatThreadData = InfiniteData<ChatThreadPage, number>;
|
||||
@@ -64,3 +65,24 @@ export function upsertMessageInThreadCache(
|
||||
|
||||
return appendMessageToThreadCache(oldData, message);
|
||||
}
|
||||
|
||||
/** هنگام refetch، پیامهای تازهٔ cache که هنوز در پاسخ API نیستند حفظ میشوند */
|
||||
export function mergeFetchedThreadPage(
|
||||
fetched: ChatMessage[],
|
||||
cachedFirstPage: ChatMessage[] | undefined
|
||||
): ChatMessage[] {
|
||||
if (!cachedFirstPage?.length) return fetched;
|
||||
|
||||
const fetchedIds = new Set(fetched.map((m) => String(m._id)));
|
||||
const preserved = cachedFirstPage.filter(
|
||||
(m) =>
|
||||
/^[a-f\d]{24}$/i.test(String(m._id)) &&
|
||||
!fetchedIds.has(String(m._id))
|
||||
);
|
||||
|
||||
if (!preserved.length) return fetched;
|
||||
|
||||
return [...fetched, ...preserved].sort(
|
||||
(a, b) => parseMessageTime(a) - parseMessageTime(b)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,15 +1,36 @@
|
||||
export const TIMED_MESSAGE_OPTIONS = [
|
||||
{ label: "خاموش", value: null as number | null },
|
||||
{ label: "۱۰ ثانیه", value: 10 },
|
||||
{ label: "۳۰ ثانیه", value: 30 },
|
||||
{ label: "۱ دقیقه", value: 60 },
|
||||
{ label: "۵ دقیقه", value: 300 },
|
||||
{ label: "۱ ساعت", value: 3600 },
|
||||
import type { TFunction } from "i18next";
|
||||
|
||||
export const TIMED_MESSAGE_SECONDS = [
|
||||
null,
|
||||
10,
|
||||
30,
|
||||
60,
|
||||
300,
|
||||
3600,
|
||||
] as const;
|
||||
|
||||
export function formatTimedLabel(seconds: number | null): string {
|
||||
const opt = TIMED_MESSAGE_OPTIONS.find((o) => o.value === seconds);
|
||||
return opt?.label ?? "خاموش";
|
||||
const TIMED_MESSAGE_KEYS = [
|
||||
"chats.timed.off",
|
||||
"chats.timed.10s",
|
||||
"chats.timed.30s",
|
||||
"chats.timed.1m",
|
||||
"chats.timed.5m",
|
||||
"chats.timed.1h",
|
||||
] as const;
|
||||
|
||||
export function getTimedMessageOptions(t: TFunction) {
|
||||
return TIMED_MESSAGE_SECONDS.map((value, index) => ({
|
||||
value,
|
||||
label: t(TIMED_MESSAGE_KEYS[index]),
|
||||
}));
|
||||
}
|
||||
|
||||
export function formatTimedLabel(seconds: number | null, t: TFunction): string {
|
||||
const index = TIMED_MESSAGE_SECONDS.indexOf(
|
||||
seconds as (typeof TIMED_MESSAGE_SECONDS)[number]
|
||||
);
|
||||
if (index >= 0) return t(TIMED_MESSAGE_KEYS[index]);
|
||||
return t("chats.timed.off");
|
||||
}
|
||||
|
||||
export function getRemainingSeconds(expiresAt?: string | null): number | null {
|
||||
|
||||
@@ -8,15 +8,20 @@ export function isViewOnceMediaType(
|
||||
return VIEW_ONCE_MEDIA_TYPES.includes(fileType as ViewOnceMediaType);
|
||||
}
|
||||
|
||||
export function viewOnceLabel(fileType?: string): string {
|
||||
import type { TFunction } from "i18next";
|
||||
|
||||
export function viewOnceLabel(
|
||||
fileType: string | undefined,
|
||||
t: TFunction
|
||||
): string {
|
||||
switch (fileType) {
|
||||
case "image":
|
||||
return "عکس یکبار مصرف";
|
||||
return t("chats.viewOnce.image");
|
||||
case "video":
|
||||
return "فیلم یکبار مصرف";
|
||||
return t("chats.viewOnce.video");
|
||||
case "voice":
|
||||
return "ویس یکبار مصرف";
|
||||
return t("chats.viewOnce.voice");
|
||||
default:
|
||||
return "پیام یکبار مصرف";
|
||||
return t("chats.viewOnce.default");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user