diff --git a/backend/src/Modules/Stories/Application/Stories/Commands/CreateStory/CreateStoryCommand.cs b/backend/src/Modules/Stories/Application/Stories/Commands/CreateStory/CreateStoryCommand.cs index 8218f95..3d9e1e7 100644 --- a/backend/src/Modules/Stories/Application/Stories/Commands/CreateStory/CreateStoryCommand.cs +++ b/backend/src/Modules/Stories/Application/Stories/Commands/CreateStory/CreateStoryCommand.cs @@ -10,7 +10,7 @@ using Knot.Modules.Stories.Domain; namespace Knot.Modules.Stories.Application.Stories.Commands.CreateStory; -public record CreateStoryCommand(Guid UserId, string Type, string? MediaUrl, string? Content, string? BgColor) : ICommand; +public record CreateStoryCommand(Guid UserId, string Type, string? MediaUrl, string? Content, string? BgColor, bool IsMuted) : ICommand; internal sealed class CreateStoryCommandHandler : ICommandHandler { @@ -27,7 +27,7 @@ internal sealed class CreateStoryCommandHandler : ICommandHandler(request.Type, true, out var parsedType)) { - parsedType = StoryType.Text; + parsedType = StoryType.Image; } // Проверка Klipy @@ -45,7 +45,8 @@ internal sealed class CreateStoryCommandHandler : ICommandHandler public string? MediaUrl { get; private set; } public string? Content { get; private set; } public string? BgColor { get; private set; } + public bool IsMuted { get; private set; } public DateTime CreatedAt { get; private set; } public int ViewsCount { get; private set; } public List Reactions { get; private set; } = new(); @@ -34,19 +35,20 @@ public class Story : Entity protected Story() : base(Guid.NewGuid()) { } - internal Story(Guid id, Guid userId, StoryType type, string? mediaUrl, string? content, string? bgColor) : base(id) + internal Story(Guid id, Guid userId, StoryType type, string? mediaUrl, string? content, string? bgColor, bool isMuted) : base(id) { UserId = userId; Type = type; MediaUrl = mediaUrl; Content = content; BgColor = bgColor; + IsMuted = isMuted; CreatedAt = DateTime.UtcNow; } - public static Story Create(Guid userId, StoryType type, string? mediaUrl, string? content, string? bgColor) + public static Story Create(Guid userId, StoryType type, string? mediaUrl, string? content, string? bgColor, bool isMuted = false) { - return new Story(Guid.NewGuid(), userId, type, mediaUrl, content, bgColor); + return new Story(Guid.NewGuid(), userId, type, mediaUrl, content, bgColor, isMuted); } public void IncrementViewsCount() diff --git a/backend/src/Modules/Stories/Presentation/Endpoints/StoriesEndpoints.cs b/backend/src/Modules/Stories/Presentation/Endpoints/StoriesEndpoints.cs index 19839f1..24a4f0d 100644 --- a/backend/src/Modules/Stories/Presentation/Endpoints/StoriesEndpoints.cs +++ b/backend/src/Modules/Stories/Presentation/Endpoints/StoriesEndpoints.cs @@ -36,7 +36,7 @@ public static class StoriesEndpoints group.MapPost("", async ([FromBody] CreateStoryRequest request, ISender sender, IUserContext userContext, CancellationToken ct) => { - var result = await sender.Send(new CreateStoryCommand(userContext.UserId, request.Type, request.MediaUrl, request.Content, request.BgColor), ct); + var result = await sender.Send(new CreateStoryCommand(userContext.UserId, request.Type, request.MediaUrl, request.Content, request.BgColor, request.IsMuted), ct); return Results.Ok(new { id = result.Value }); }); diff --git a/client-web/src/core/domain/types.ts b/client-web/src/core/domain/types.ts index 3bf51bc..185df66 100644 --- a/client-web/src/core/domain/types.ts +++ b/client-web/src/core/domain/types.ts @@ -150,6 +150,7 @@ export interface Story { mediaUrl: string | null; content: string | null; bgColor: string | null; + isMuted?: boolean; createdAt: string; expiresAt: string; viewCount: number; diff --git a/client-web/src/core/presentation/layouts/GlobalNavBar.tsx b/client-web/src/core/presentation/layouts/GlobalNavBar.tsx index 0c9c8e1..b67973a 100644 --- a/client-web/src/core/presentation/layouts/GlobalNavBar.tsx +++ b/client-web/src/core/presentation/layouts/GlobalNavBar.tsx @@ -13,7 +13,6 @@ export default function GlobalNavBar({ activeTab, onTabChange }: GlobalNavBarPro const menuItems = [ { id: 'chats', icon: 'chat', label: t('chats') }, { id: 'contacts', icon: 'contacts', label: t('contacts') }, - { id: 'settings', icon: 'settings', label: t('settings') }, ]; return ( diff --git a/client-web/src/modules/chats/presentation/components/ChatView.tsx b/client-web/src/modules/chats/presentation/components/ChatView.tsx index b7446de..17f36f2 100644 --- a/client-web/src/modules/chats/presentation/components/ChatView.tsx +++ b/client-web/src/modules/chats/presentation/components/ChatView.tsx @@ -191,25 +191,22 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal const scrollTimeoutRef = useRef(null); const prevChatIdRef = useRef(activeChat); - // 1. СОХРАНЕНИЕ ПОЗИЦИИ (СТРОГО ПО ID) + // 1. СОХРАНЕНИЕ ПОЗИЦИИ (ЯКОРНОЕ ПО MESSAGE ID) const saveScrollPosition = useCallback((targetChatId?: string) => { const container = messagesContainerRef.current; const chatId = targetChatId || activeChat; - // НЕ сохраняем, если чат ещё не восстановил свою позицию или в процессе загрузки if (!container || !chatId || !scrollReady || isInitializingRef.current || isScrollingToBottomRef.current) return; - // ГАРАНТИЯ: Если сообщения в стейте НЕ от этого чата - не пишем в память мусор + // Проверка: сообщения в стейте должны быть от целевого чата if (chatMessages.length > 0 && chatMessages[0].chatId !== chatId) return; - // КРИТИЧНО: Если этот чат уже помечен как находящийся внизу, - // не позволяем автоматике перезаписать это якорем (защита "второго клика") - if (localStorage.getItem(`chat_at_bottom_${chatId}`) === 'true') { - const isNearBottomNow = container.scrollHeight - container.scrollTop - container.clientHeight < 150; - if (isNearBottomNow) { - localStorage.removeItem(`chat_anchor_${chatId}`); - return; - } + const isAtBottomNow = container.scrollHeight - container.scrollTop - container.clientHeight < 150; + + if (isAtBottomNow) { + localStorage.setItem(`chat_at_bottom_${chatId}`, 'true'); + localStorage.removeItem(`chat_anchor_${chatId}`); + return; } const messageElements = container.querySelectorAll('[data-message-id]'); @@ -218,9 +215,10 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal let anchor = null; const containerRect = container.getBoundingClientRect(); + // Находим первое сообщение, которое пересекает верхнюю границу видимости for (const el of messageElements) { const rect = el.getBoundingClientRect(); - if (rect.top >= containerRect.top) { + if (rect.bottom > containerRect.top) { anchor = { id: el.getAttribute('data-message-id'), offset: rect.top - containerRect.top @@ -231,13 +229,6 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal if (anchor && anchor.id) { localStorage.setItem(`chat_anchor_${chatId}`, JSON.stringify(anchor)); - } - - const isAtBottomNow = container.scrollHeight - container.scrollTop - container.clientHeight < 150; - if (isAtBottomNow) { - localStorage.setItem(`chat_at_bottom_${chatId}`, 'true'); - localStorage.removeItem(`chat_anchor_${chatId}`); - } else { localStorage.removeItem(`chat_at_bottom_${chatId}`); } }, [activeChat, scrollReady, chatMessages]); @@ -247,32 +238,23 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal const container = messagesContainerRef.current; if (!container || !activeChat) return false; - // КРИТИЧНО: Ждем, пока в хранилище сообщений появятся данные именно от активного чата - if (chatMessages.length > 0 && chatMessages[0].chatId !== activeChat) { - return false; - } - + if (chatMessages.length > 0 && chatMessages[0].chatId !== activeChat) return false; if (chatMessages.length === 0) return true; - // Сначала проверяем, был ли пользователь внизу (защита "второго клика") + // ПРИОРИТЕТ 1: Если чат внизу (после второго клика или скролла) if (localStorage.getItem(`chat_at_bottom_${activeChat}`) === 'true') { - scrollToBottom(false); - - // Агрессивные повторы, если контент еще догружается (картинки и т.д.) - for (const delay of [100, 300, 600, 1000]) { - setTimeout(() => { - if (activeChat === prevChatIdRef.current) scrollToBottom(false); - }, delay); - } + container.scrollTop = container.scrollHeight; return true; } - // Восстановление по якорю сообщения + // ПРИОРИТЕТ 2: Восстановление по якорю сообщения const saved = localStorage.getItem(`chat_anchor_${activeChat}`); if (saved) { try { const { id, offset } = JSON.parse(saved); let el = container.querySelector(`[data-message-id="${id}"]`) as HTMLElement; + + // Поиск ближайшего, если точное сообщение еще не загружено if (!el) { const msgIndex = chatMessages.findIndex(m => m.id === id); if (msgIndex !== -1) { @@ -282,15 +264,15 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal } } } + if (el) { container.scrollTop = el.offsetTop - offset; return true; } - if (chatMessages.some(m => m.id === id)) return false; - } catch (e) { console.error(e); } + } catch (e) { console.error('Anchor restoration failed', e); } } - // Если ничего нет - к непрочитанным или вниз + // ПРИОРИТЕТ 3: Непрочитанные или низ const firstUnread = chatMessages.find(m => m.senderId !== user?.id && !m.readBy?.some(r => r.userId === user?.id)); if (firstUnread) { const el = document.getElementById(`msg-${firstUnread.id}`) || document.getElementById('unread-divider'); @@ -299,17 +281,17 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal return true; } } - scrollToBottom(false); - return true; - }, [activeChat, chatMessages, scrollToBottom, user?.id]); - // 3. ОБЗЕРВЕР И ИНИЦИАЛИЗАЦИЯ + container.scrollTop = container.scrollHeight; + return true; + }, [activeChat, chatMessages, user?.id]); + + // 3. ОБЗЕРВЕР И УПРАВЛЕНИЕ ЖИЗНЕННЫМ ЦИКЛОМ useEffect(() => { if (isLoadingMessages || !messagesContainerRef.current || !activeChat) return; const container = messagesContainerRef.current; const observer = new ResizeObserver(() => { - // Игнорируем замеры, пока мы не отпозиционировали чат изначально if (isInitializingRef.current) return; if (!scrollReady) { @@ -318,17 +300,15 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal isInitializingRef.current = false; } } else if (localStorage.getItem(`chat_at_bottom_${activeChat}`) === 'true') { - const isNearBottomNow = container.scrollHeight - container.scrollTop - container.clientHeight < 300; - if (isNearBottomNow) { - scrollToBottom(true); - } + // Удержание внизу при росте контента + container.scrollTop = container.scrollHeight; } }); const messagesDiv = container.querySelector('.space-y-1'); if (messagesDiv) observer.observe(messagesDiv); - // Принудительно пробуем восстановить, если сообщения ПРАВИЛЬНЫЕ + // Попытка восстановления при появлении правильных сообщений if (chatMessages.length > 0 && chatMessages[0].chatId === activeChat && !scrollReady) { if (restoreScrollPosition()) { setScrollReady(true); @@ -337,30 +317,29 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal } return () => observer.disconnect(); - }, [activeChat, isLoadingMessages, chatMessages, restoreScrollPosition, scrollReady, scrollToBottom]); + }, [activeChat, isLoadingMessages, chatMessages, restoreScrollPosition, scrollReady]); - // 4. ПЕРЕКЛЮЧЕНИЕ ЧАТОВ (ФИКС RACE CONDITION) + // 4. ПЕРЕКЛЮЧЕНИЕ ЧАТОВ (СИНХРОННОЕ) useLayoutEffect(() => { if (activeChat !== prevChatIdRef.current) { - // СРАЗУ блокируем сохранение, чтобы handleScroll ничего не записал при изменении высоты isInitializingRef.current = true; + isScrollingToBottomRef.current = false; - // Сохраняем позицию ПРЕДЫДУЩЕГО чата ПЕРЕД установкой новых данных + // Сохраняем позицию старого чата if (prevChatIdRef.current && messagesContainerRef.current && scrollReady) { - saveScrollPosition(prevChatIdRef.current); + saveScrollPosition(prevChatIdRef.current); } setScrollReady(false); prevChatIdRef.current = activeChat; if (messagesContainerRef.current) { - messagesContainerRef.current.scrollTop = 0; + messagesContainerRef.current.scrollTop = 0; } - // Таймер защиты на случай медленного рендера const timer = setTimeout(() => { - isInitializingRef.current = false; - }, 1000); + isInitializingRef.current = false; + }, 600); return () => clearTimeout(timer); } @@ -374,17 +353,13 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal }, []); const handleScroll = () => { - // В период инициализации (1.2с) или принудительного скролла вниз - игнорируем любые события. if (isInitializingRef.current || isScrollingToBottomRef.current) return; checkScrollPosition(); const container = messagesContainerRef.current; if (container && activeChat) { - const isNearBottomNow = container.scrollHeight - container.scrollTop - container.clientHeight < 120; - if (!isNearBottomNow) localStorage.removeItem(`chat_at_bottom_${activeChat}`); - if (scrollTimeoutRef.current) clearTimeout(scrollTimeoutRef.current); - scrollTimeoutRef.current = setTimeout(() => saveScrollPosition(), 200); + scrollTimeoutRef.current = setTimeout(() => saveScrollPosition(), 150); if (container.scrollTop < 100 && hasMoreMessages[activeChat] && !isLoadingMessages) { useChatStore.getState().loadMessages(activeChat, false, true); @@ -395,18 +370,26 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal useEffect(() => { const handleScrollEvent = (e: any) => { if (e.detail?.chatId === activeChat) { - isScrollingToBottomRef.current = true; + // Принудительный сброс режима (Второй Клик) localStorage.setItem(`chat_at_bottom_${activeChat}`, 'true'); localStorage.removeItem(`chat_anchor_${activeChat}`); - scrollToBottom(true); + + isScrollingToBottomRef.current = true; + if (messagesContainerRef.current) { + messagesContainerRef.current.scrollTo({ + top: messagesContainerRef.current.scrollHeight, + behavior: 'smooth' + }); + } + setTimeout(() => { isScrollingToBottomRef.current = false; - }, 500); + }, 1000); } }; window.addEventListener('CHAT_SCROLL_TO_BOTTOM', handleScrollEvent); return () => window.removeEventListener('CHAT_SCROLL_TO_BOTTOM', handleScrollEvent); - }, [activeChat, scrollToBottom]); + }, [activeChat]); // Read receipts using IntersectionObserver const sentReadIdsRef = useRef>(new Set()); @@ -426,7 +409,7 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal let highestSequenceId = -1; let highestMsgId = ''; const newlyReadIds: string[] = []; - + entries.forEach((entry) => { if (entry.isIntersecting) { const msgId = entry.target.getAttribute('data-message-id'); @@ -435,7 +418,7 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal newlyReadIds.push(msgId); sentReadIdsRef.current.add(msgId); observer.unobserve(entry.target); - + const seqId = parseInt(seqIdAttr, 10); if (seqId > highestSequenceId) { highestSequenceId = seqId; @@ -568,16 +551,16 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal forum - +

{t('selectChatTitle')}

- +

{t('selectChatSubtext')}

- 0 ? new Date(chatMessages[0].createdAt).getTime() : Date.now(); - + // If target is newer than oldest loaded, and not found, maybe it's in a gap or we need to keep loading? // Actually target is almost always older if not found. // If we don't have targetCreatedAt, we guess (up to 100 attempts) if (targetDate && targetDate > oldestLoaded && chatMessages.some(m => m.id === msgId)) { - // Should have been found by tryScroll, but lets try one last time - if (tryScroll()) { found = true; break; } + // Should have been found by tryScroll, but lets try one last time + if (tryScroll()) { found = true; break; } } if (chatStore.hasMoreMessages[activeChat] === false && oldestLoaded <= targetDate) break; - + await chatStore.loadMessages(activeChat, false, true); // Give React 150ms to render the new messages await new Promise(resolve => setTimeout(resolve, 150)); - + if (tryScroll()) { found = true; break; @@ -1220,11 +1203,11 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal // Stop if we have gone way past the target date if (targetDate && oldestLoaded < (targetDate - 1000 * 60 * 60)) { - // We are 1 hour before the message and still haven't found it? might be deleted - if (i > 10) break; + // We are 1 hour before the message and still haven't found it? might be deleted + if (i > 10) break; } } - + if (!found) { NotificationStore.useNotificationStore.getState().addNotification('warning', lang === 'ru' ? 'Сообщение не найдено' : 'Message not found'); } diff --git a/client-web/src/modules/stories/infrastructure/storyApi.ts b/client-web/src/modules/stories/infrastructure/storyApi.ts index d809fc3..620fcf4 100644 --- a/client-web/src/modules/stories/infrastructure/storyApi.ts +++ b/client-web/src/modules/stories/infrastructure/storyApi.ts @@ -10,7 +10,7 @@ export class StoryApi { return httpClient.request(`/stories/user/${userId}`); } - static async createStory(data: { type: string; mediaUrl?: string; content?: string; bgColor?: string; privacy?: string; filter?: string }) { + static async createStory(data: { type: string; mediaUrl?: string; content?: string; bgColor?: string; privacy?: string; filter?: string; isMuted?: boolean }) { return httpClient.request<{ id: string }>('/stories', { method: 'POST', body: JSON.stringify(data), diff --git a/client-web/src/modules/stories/presentation/components/CreateStoryModal.tsx b/client-web/src/modules/stories/presentation/components/CreateStoryModal.tsx index bc14926..07dcfa5 100644 --- a/client-web/src/modules/stories/presentation/components/CreateStoryModal.tsx +++ b/client-web/src/modules/stories/presentation/components/CreateStoryModal.tsx @@ -44,6 +44,7 @@ export function CreateStoryModal({ onClose, onCreated }: CreateStoryModalProps) const [croppedPreview, setCroppedPreview] = useState(null); const [bgColor, setBgColor] = useState('#1e1e2e'); const [isUploading, setIsUploading] = useState(false); + const [isMuted, setIsMuted] = useState(false); const [tool, setTool] = useState<'none' | 'crop' | 'brush' | 'stickers' | 'filters' | 'privacy' | 'text'>('none'); const [privacy, setPrivacy] = useState<'all' | 'contacts' | 'selected'>('all'); @@ -88,7 +89,7 @@ export function CreateStoryModal({ onClose, onCreated }: CreateStoryModalProps) const url = URL.createObjectURL(file); setMediaPreview(url); setCroppedPreview(null); - setTool('crop'); + setTool(file.type.startsWith('video/') ? 'none' : 'crop'); }; const removeMedia = () => { @@ -164,66 +165,86 @@ export function CreateStoryModal({ onClose, onCreated }: CreateStoryModalProps) const handlePublish = async () => { setIsUploading(true); try { - const canvas = document.createElement('canvas'); - canvas.width = TARGET_W; - canvas.height = TARGET_H; - const ctx = canvas.getContext('2d'); - if (!ctx) throw new Error('Canvas init failed'); + const isVideo = mediaFile?.type.startsWith('video/'); + let url = ''; + let type = isVideo ? 'video' : 'image'; + let content = ''; - // 1. BG Color - ctx.fillStyle = bgColor; - ctx.fillRect(0, 0, TARGET_W, TARGET_H); + if (isVideo && mediaFile) { + // Handle Video: upload original file + const res = await StoryApi.uploadVideoToStory(mediaFile); + url = res.url; + // Serialize overlays as metadata + content = JSON.stringify({ + stickers: stickers.map(s => ({ ...s, emoji: s.emoji })), // Ensure emoji is string + textObjects: textObjects.map(t => ({ ...t })) + }); + } else { + // Handle Image/Text: bake into canvas + const canvas = document.createElement('canvas'); + canvas.width = TARGET_W; + canvas.height = TARGET_H; + const ctx = canvas.getContext('2d'); + if (!ctx) throw new Error('Canvas init failed'); - // 2. Base Image Layer - const baseSrc = croppedPreview || mediaPreview; - if (baseSrc && (mediaFile?.type.startsWith('image/') || !mediaFile)) { - const img = await loadImage(baseSrc); - ctx.save(); - ctx.filter = getFilterCss(); - - if (!croppedPreview && croppedAreaPixels) { - ctx.drawImage(img, croppedAreaPixels.x, croppedAreaPixels.y, croppedAreaPixels.width, croppedAreaPixels.height, 0, 0, TARGET_W, TARGET_H); - } else { - ctx.drawImage(img, 0, 0, TARGET_W, TARGET_H); + // 1. BG Color + ctx.fillStyle = bgColor; + ctx.fillRect(0, 0, TARGET_W, TARGET_H); + + // 2. Base Image Layer + const baseSrc = croppedPreview || mediaPreview; + if (baseSrc && (mediaFile?.type.startsWith('image/') || !mediaFile)) { + const img = await loadImage(baseSrc); + ctx.save(); + ctx.filter = getFilterCss(); + + if (!croppedPreview && croppedAreaPixels) { + ctx.drawImage(img, croppedAreaPixels.x, croppedAreaPixels.y, croppedAreaPixels.width, croppedAreaPixels.height, 0, 0, TARGET_W, TARGET_H); + } else { + ctx.drawImage(img, 0, 0, TARGET_W, TARGET_H); + } + ctx.restore(); } - ctx.restore(); + + // 3. Brush Layer + if (canvasRef.current) { + ctx.drawImage(canvasRef.current, 0, 0, TARGET_W, TARGET_H); + } + + // 4. Overlays + ctx.textAlign = 'left'; + ctx.textBaseline = 'middle'; + + stickers.forEach(s => { + ctx.font = `${s.scale * 100}px "Apple Color Emoji", "Segoe UI Emoji", serif`; + ctx.fillText(s.emoji, (s.x / 100) * TARGET_W, (s.y / 100) * TARGET_H); + }); + + textObjects.forEach(t => { + const fontSize = t.size * SCALE_FACTOR * t.scale; + ctx.font = `bold ${fontSize}px ${t.font}`; + ctx.fillStyle = t.color; + ctx.fillText(t.text, (t.x / 100) * TARGET_W, (t.y / 100) * TARGET_H); + }); + + const blob = await new Promise((resolve, reject) => { + canvas.toBlob(b => b ? resolve(b) : reject('Blob error'), 'image/jpeg', 0.95); + }); + + const fileToUpload = new File([blob], `story_${Date.now()}.jpg`, { type: 'image/jpeg' }); + const uploadRes = await ChatApi.uploadFile(fileToUpload); + url = uploadRes.url; } - // 3. Brush Layer - if (canvasRef.current) { - ctx.drawImage(canvasRef.current, 0, 0, TARGET_W, TARGET_H); - } - - // 4. Overlays - ctx.textAlign = 'left'; - ctx.textBaseline = 'middle'; - - stickers.forEach(s => { - ctx.font = `${s.scale * 100}px "Apple Color Emoji", "Segoe UI Emoji", serif`; - ctx.fillText(s.emoji, (s.x / 100) * TARGET_W, (s.y / 100) * TARGET_H); - }); - - textObjects.forEach(t => { - const fontSize = t.size * SCALE_FACTOR * t.scale; - ctx.font = `bold ${fontSize}px ${t.font}`; - ctx.fillStyle = t.color; - ctx.fillText(t.text, (t.x / 100) * TARGET_W, (t.y / 100) * TARGET_H); - }); - - const blob = await new Promise((resolve, reject) => { - canvas.toBlob(b => b ? resolve(b) : reject('Blob error'), 'image/jpeg', 0.95); - }); - - const fileToUpload = new File([blob], `story_${Date.now()}.jpg`, { type: 'image/jpeg' }); - const { url } = await ChatApi.uploadFile(fileToUpload); if (!url) throw new Error('Upload failed'); - // Using PascalCase matching the DTO just in case, and including original content await StoryApi.createStory({ - type: 'image', + type, mediaUrl: url, bgColor: bgColor, - privacy: privacy, // Note: might not be in backend but safe to send + content: content, + privacy: privacy, + isMuted: isVideo ? isMuted : false, } as any); onCreated(); @@ -242,12 +263,20 @@ export function CreateStoryModal({ onClose, onCreated }: CreateStoryModalProps) `}
-
-
S
-

Stories Editor

+
+ +

{t('storiesEditor')}

- + {mediaFile?.type.startsWith('video/') && ( + + )} + @@ -261,7 +290,9 @@ export function CreateStoryModal({ onClose, onCreated }: CreateStoryModalProps) {(croppedPreview || mediaPreview) && (
{mediaFile?.type.startsWith('video/') ? ( -