Initial commit - modstagram-next
This commit is contained in:
19
src/lib/chat/dedupeMessages.ts
Normal file
19
src/lib/chat/dedupeMessages.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import type { ChatMessage } from "@/components/chat/ChatMessageCard";
|
||||
|
||||
export function dedupeChatMessages(messages: ChatMessage[]): ChatMessage[] {
|
||||
const seen = new Set<string>();
|
||||
const result: ChatMessage[] = [];
|
||||
|
||||
for (const m of messages) {
|
||||
if (!m) continue;
|
||||
const idKey =
|
||||
m._id && /^[a-f\d]{24}$/i.test(m._id)
|
||||
? m._id
|
||||
: `${m.senderId}|${m.content}|${m.file}|${m.createdAt}`;
|
||||
if (seen.has(idKey)) continue;
|
||||
seen.add(idKey);
|
||||
result.push(m);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
41
src/lib/chat/detectFileType.ts
Normal file
41
src/lib/chat/detectFileType.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import type { ChatMessage } from "@/components/chat/ChatMessageCard";
|
||||
|
||||
const IMAGE_EXT = ["jpg", "jpeg", "png", "webp", "gif", "avif"];
|
||||
const VIDEO_EXT = ["mp4", "webm", "mov", "m4v"];
|
||||
const VOICE_EXT = ["mp3", "wav", "aac", "m4a", "ogg", "opus", "webm"];
|
||||
const DOC_EXT = ["pdf", "zip", "rar", "doc", "docx", "xls", "xlsx", "txt"];
|
||||
|
||||
/** Reliable media type — voice never shown as video */
|
||||
export function detectChatFileType(
|
||||
fileUrl: string,
|
||||
declared?: ChatMessage["fileType"]
|
||||
): ChatMessage["fileType"] | null {
|
||||
if (declared === "location") return "location";
|
||||
if (declared === "voice") return "voice";
|
||||
if (!fileUrl) return declared ?? null;
|
||||
|
||||
const lower = fileUrl.toLowerCase();
|
||||
const ext = lower.split(".").pop()?.split("?")[0] || "";
|
||||
|
||||
if (declared === "image" || IMAGE_EXT.includes(ext)) return "image";
|
||||
if (declared === "voice") return "voice";
|
||||
if (VOICE_EXT.includes(ext) && ext !== "webm") return "voice";
|
||||
if (ext === "webm") {
|
||||
if (
|
||||
lower.includes("voice") ||
|
||||
lower.includes("audio") ||
|
||||
declared === "voice"
|
||||
) {
|
||||
return "voice";
|
||||
}
|
||||
if (declared === "video" || lower.includes("video")) return "video";
|
||||
return "voice";
|
||||
}
|
||||
if (declared === "video" || VIDEO_EXT.includes(ext)) return "video";
|
||||
if (declared === "file" || DOC_EXT.includes(ext)) return "file";
|
||||
|
||||
if (IMAGE_EXT.includes(ext)) return "image";
|
||||
if (["mp3", "wav", "aac", "m4a", "opus"].includes(ext)) return "voice";
|
||||
if (VIDEO_EXT.includes(ext)) return "video";
|
||||
return "file";
|
||||
}
|
||||
36
src/lib/chat/formatMessageTime.ts
Normal file
36
src/lib/chat/formatMessageTime.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
/** Bubble time: hours, minutes, seconds only (no date) */
|
||||
export function formatBubbleTime(createdAt?: string | null): string {
|
||||
if (!createdAt) {
|
||||
return new Date().toLocaleTimeString("fa-IR", {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
});
|
||||
}
|
||||
|
||||
const raw = String(createdAt).trim();
|
||||
|
||||
// Jalali datetime "1403-01-15 14:30:45" or ISO
|
||||
const timePart = raw.includes(" ")
|
||||
? raw.split(" ").pop()
|
||||
: raw.length <= 8 && raw.includes(":")
|
||||
? raw
|
||||
: null;
|
||||
|
||||
if (timePart && /^\d{1,2}:\d{2}(:\d{2})?$/.test(timePart)) {
|
||||
const parts = timePart.split(":");
|
||||
if (parts.length === 2) return `${parts[0]}:${parts[1]}:00`;
|
||||
return timePart;
|
||||
}
|
||||
|
||||
const d = new Date(raw);
|
||||
if (!Number.isNaN(d.getTime())) {
|
||||
return d.toLocaleTimeString("fa-IR", {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
});
|
||||
}
|
||||
|
||||
return raw;
|
||||
}
|
||||
67
src/lib/chat/groupMessagesByDate.ts
Normal file
67
src/lib/chat/groupMessagesByDate.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import type { ChatMessage } from "@/components/chat/ChatMessageCard";
|
||||
|
||||
export type MessageGroup =
|
||||
| { type: "date"; label: string; key: string }
|
||||
| { type: "message"; message: ChatMessage; key: string };
|
||||
|
||||
function normalizeDateInput(isoOrTime?: string | null): string {
|
||||
if (isoOrTime == null || String(isoOrTime).trim() === "") {
|
||||
return new Date().toISOString();
|
||||
}
|
||||
return String(isoOrTime);
|
||||
}
|
||||
|
||||
function formatDateLabel(isoOrTime?: string | null): string {
|
||||
const raw = normalizeDateInput(isoOrTime);
|
||||
const d = new Date(raw);
|
||||
if (!Number.isNaN(d.getTime())) {
|
||||
const today = new Date();
|
||||
const yesterday = new Date();
|
||||
yesterday.setDate(today.getDate() - 1);
|
||||
if (d.toDateString() === today.toDateString()) return "امروز";
|
||||
if (d.toDateString() === yesterday.toDateString()) return "دیروز";
|
||||
return d.toLocaleDateString("fa-IR", {
|
||||
weekday: "long",
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
});
|
||||
}
|
||||
const fallback = raw.split(" ")[0];
|
||||
return fallback || "امروز";
|
||||
}
|
||||
|
||||
function dayKey(isoOrTime?: string | null): string {
|
||||
const raw = normalizeDateInput(isoOrTime);
|
||||
const d = new Date(raw);
|
||||
if (!Number.isNaN(d.getTime())) {
|
||||
return d.toDateString();
|
||||
}
|
||||
return raw.split(" ")[0] || "unknown-day";
|
||||
}
|
||||
|
||||
export function groupMessagesByDate(messages: ChatMessage[]): MessageGroup[] {
|
||||
const groups: MessageGroup[] = [];
|
||||
let lastDay = "";
|
||||
|
||||
messages.forEach((msg, i) => {
|
||||
if (!msg) return;
|
||||
|
||||
const dk = dayKey(msg.createdAt);
|
||||
if (dk !== lastDay) {
|
||||
lastDay = dk;
|
||||
groups.push({
|
||||
type: "date",
|
||||
label: formatDateLabel(msg.createdAt),
|
||||
key: `date-${dk}`,
|
||||
});
|
||||
}
|
||||
groups.push({
|
||||
type: "message",
|
||||
message: msg,
|
||||
key: msg._id || `m-${i}`,
|
||||
});
|
||||
});
|
||||
|
||||
return groups;
|
||||
}
|
||||
Reference in New Issue
Block a user