Редактирование и плеер
This commit is contained in:
@@ -0,0 +1,69 @@
|
|||||||
|
using Knot.Contracts.Messaging.Application.Abstractions;
|
||||||
|
using Knot.Contracts.Messaging.Domain;
|
||||||
|
using Knot.Contracts.Conversations.Application.Abstractions;
|
||||||
|
using Knot.Contracts.Conversations.Domain;
|
||||||
|
using Knot.Modules.Conversations.Infrastructure.SignalR;
|
||||||
|
using Knot.Shared.Kernel;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.AspNetCore.SignalR;
|
||||||
|
|
||||||
|
namespace Knot.Modules.Conversations.Application.Messages.Edit;
|
||||||
|
|
||||||
|
public sealed record EditMessageCommand(
|
||||||
|
Guid MessageId,
|
||||||
|
Guid ChatId,
|
||||||
|
Guid UserId,
|
||||||
|
string Content) : ICommand;
|
||||||
|
|
||||||
|
public sealed class EditMessageCommandHandler : ICommandHandler<EditMessageCommand>
|
||||||
|
{
|
||||||
|
private readonly IMessageRepository _messageRepository;
|
||||||
|
private readonly IChatsUnitOfWork _unitOfWork;
|
||||||
|
private readonly IHubContext<ChatHub> _hubContext;
|
||||||
|
|
||||||
|
public EditMessageCommandHandler(
|
||||||
|
IMessageRepository messageRepository,
|
||||||
|
IChatsUnitOfWork unitOfWork,
|
||||||
|
IHubContext<ChatHub> hubContext)
|
||||||
|
{
|
||||||
|
_messageRepository = messageRepository;
|
||||||
|
_unitOfWork = unitOfWork;
|
||||||
|
_hubContext = hubContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Result> Handle(EditMessageCommand request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var message = await _messageRepository.GetByIdAsync(request.MessageId, cancellationToken);
|
||||||
|
|
||||||
|
if (message is null)
|
||||||
|
{
|
||||||
|
return Result.Failure(new Error("Message.NotFound", "Message not found."));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.SenderId != request.UserId)
|
||||||
|
{
|
||||||
|
return Result.Failure(new Error("Message.Forbidden", "You can only edit your own messages."));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.ChatId != request.ChatId)
|
||||||
|
{
|
||||||
|
return Result.Failure(new Error("Message.InvalidChat", "Message does not belong to this chat."));
|
||||||
|
}
|
||||||
|
|
||||||
|
message.Edit(request.Content);
|
||||||
|
|
||||||
|
await _messageRepository.UpdateAsync(message, cancellationToken);
|
||||||
|
await _unitOfWork.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
// Notify clients
|
||||||
|
await _hubContext.Clients.Group(request.ChatId.ToString()).SendAsync("message_edited", new
|
||||||
|
{
|
||||||
|
messageId = message.Id,
|
||||||
|
chatId = message.ChatId,
|
||||||
|
content = message.Content,
|
||||||
|
isEdited = true
|
||||||
|
});
|
||||||
|
|
||||||
|
return Result.Success();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,6 +16,7 @@ using Knot.Contracts.Auth.Application.Abstractions;
|
|||||||
using Knot.Modules.Conversations.Application.Messages.Pin;
|
using Knot.Modules.Conversations.Application.Messages.Pin;
|
||||||
using Knot.Modules.Conversations.Application.Messages.Unpin;
|
using Knot.Modules.Conversations.Application.Messages.Unpin;
|
||||||
using Knot.Modules.Conversations.Application.Messages.Vote;
|
using Knot.Modules.Conversations.Application.Messages.Vote;
|
||||||
|
using Knot.Modules.Conversations.Application.Messages.Edit;
|
||||||
using Knot.Modules.Conversations.Application.DTOs;
|
using Knot.Modules.Conversations.Application.DTOs;
|
||||||
using Knot.Contracts.Messaging.Application.Abstractions;
|
using Knot.Contracts.Messaging.Application.Abstractions;
|
||||||
using Knot.Contracts.Messaging.Domain;
|
using Knot.Contracts.Messaging.Domain;
|
||||||
@@ -284,6 +285,17 @@ public sealed class ChatHub : Hub
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HubMethodName("edit_message")]
|
||||||
|
public async Task EditMessage(EditMessageHubRequest request)
|
||||||
|
{
|
||||||
|
var command = new EditMessageCommand(request.MessageId, request.ChatId, _userContext.UserId, request.Content);
|
||||||
|
var result = await _sender.Send(command);
|
||||||
|
if (result.IsFailure)
|
||||||
|
{
|
||||||
|
throw new HubException(result.Error.Description);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[HubMethodName("vote_poll")]
|
[HubMethodName("vote_poll")]
|
||||||
public async Task VotePoll(VotePollRequest request)
|
public async Task VotePoll(VotePollRequest request)
|
||||||
{
|
{
|
||||||
@@ -856,6 +868,7 @@ public sealed class ChatHub : Hub
|
|||||||
public record GroupRenegotiateAnswerRequest(string ChatId, string TargetUserId, object Answer);
|
public record GroupRenegotiateAnswerRequest(string ChatId, string TargetUserId, object Answer);
|
||||||
public record FriendSignalRequest(string FriendId);
|
public record FriendSignalRequest(string FriendId);
|
||||||
public record VotePollRequest(Guid MessageId, Guid ChatId, Guid OptionId);
|
public record VotePollRequest(Guid MessageId, Guid ChatId, Guid OptionId);
|
||||||
|
public record EditMessageHubRequest(Guid MessageId, Guid ChatId, string Content);
|
||||||
|
|
||||||
public class CallSession
|
public class CallSession
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -148,8 +148,8 @@ export default function ChatPage() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('message_edited', (message: Message) => {
|
socket.on('message_edited', (data: { messageId: string; chatId: string; content: string; isEdited: boolean }) => {
|
||||||
updateMessage(message);
|
updateMessage({ id: data.messageId, chatId: data.chatId, content: data.content, isEdited: data.isEdited } as Message);
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('new_chat', (chat: any) => {
|
socket.on('new_chat', (chat: any) => {
|
||||||
|
|||||||
@@ -25,13 +25,14 @@ import {
|
|||||||
PhoneIncoming,
|
PhoneIncoming,
|
||||||
PhoneOutgoing,
|
PhoneOutgoing,
|
||||||
BarChart2,
|
BarChart2,
|
||||||
|
Music,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { useAuthStore } from '../../../auth/application/authStore';
|
import { useAuthStore } from '../../../auth/application/authStore';
|
||||||
import { useChatStore } from '../../application/chatStore';
|
import { useChatStore } from '../../application/chatStore';
|
||||||
import { getSocket } from '../../../../core/infrastructure/socket';
|
import { getSocket } from '../../../../core/infrastructure/socket';
|
||||||
import { useLang } from '../../../../core/infrastructure/i18n';
|
import { useLang } from '../../../../core/infrastructure/i18n';
|
||||||
import { extractWaveform, getMediaUrl, generateAvatarColor, getInitials } from '../../../../core/utils/utils';
|
import { extractWaveform, getMediaUrl, generateAvatarColor, getInitials } from '../../../../core/utils/utils';
|
||||||
import type { Message, MediaItem, Reaction, ChatMember } from '../../../../core/domain/types';
|
import { AUDIO_EXTENSIONS, type Message, type MediaItem, type Reaction, type ChatMember } from '../../../../core/domain/types';
|
||||||
import ImageLightbox from '../../../../core/presentation/components/ui/ImageLightbox';
|
import ImageLightbox from '../../../../core/presentation/components/ui/ImageLightbox';
|
||||||
import LinkPreview from './LinkPreview';
|
import LinkPreview from './LinkPreview';
|
||||||
import Avatar from '../../../../core/presentation/components/ui/Avatar';
|
import Avatar from '../../../../core/presentation/components/ui/Avatar';
|
||||||
@@ -355,11 +356,13 @@ function MessageBubble({
|
|||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isAudioFile = (m: MediaItem) => m.type === 'audio' || AUDIO_EXTENSIONS.some(ext => m.filename?.toLowerCase().endsWith(ext));
|
||||||
|
|
||||||
const hasImage = media.some((m) => m.type === 'image' || isMediaGif(m));
|
const hasImage = media.some((m) => m.type === 'image' || isMediaGif(m));
|
||||||
const hasVoice = message.type === 'voice' || media.some((m) => m.type === 'voice');
|
const hasVoice = message.type === 'voice' || media.some((m) => m.type === 'voice');
|
||||||
const hasAudio = !hasVoice && (message.type === 'audio' || media.some((m) => m.type === 'audio'));
|
const hasAudio = !hasVoice && (message.type === 'audio' || media.some(isAudioFile));
|
||||||
const hasVideo = media.some((m) => m.type === 'video' && !isMediaGif(m));
|
const hasVideo = media.some((m) => m.type === 'video' && !isMediaGif(m));
|
||||||
const hasFile = media.some((m) => m.type !== 'image' && m.type !== 'voice' && m.type !== 'video' && m.type !== 'audio' && !isMediaGif(m));
|
const hasFile = media.some((m) => m.type !== 'image' && m.type !== 'voice' && m.type !== 'video' && !isAudioFile(m) && !isMediaGif(m));
|
||||||
|
|
||||||
const reactionGroups: Record<string, { count: number; users: string[]; isMine: boolean; avatars: { url?: string | null, initials: string, colorClass?: string }[] }> = {};
|
const reactionGroups: Record<string, { count: number; users: string[]; isMine: boolean; avatars: { url?: string | null, initials: string, colorClass?: string }[] }> = {};
|
||||||
(message.reactions || []).forEach((r) => {
|
(message.reactions || []).forEach((r) => {
|
||||||
@@ -765,7 +768,7 @@ function MessageBubble({
|
|||||||
|
|
||||||
{/* Аудио (mp3 файлы) */}
|
{/* Аудио (mp3 файлы) */}
|
||||||
{hasAudio && (() => {
|
{hasAudio && (() => {
|
||||||
const audioMedia = media.find((m) => m.type === 'audio');
|
const audioMedia = media.find(isAudioFile);
|
||||||
const formatSize = (bytes?: number | null) => {
|
const formatSize = (bytes?: number | null) => {
|
||||||
if (!bytes) return "";
|
if (!bytes) return "";
|
||||||
if (bytes < 1024) return bytes + " B";
|
if (bytes < 1024) return bytes + " B";
|
||||||
@@ -855,7 +858,7 @@ function MessageBubble({
|
|||||||
{/* Файлы */}
|
{/* Файлы */}
|
||||||
{hasFile &&
|
{hasFile &&
|
||||||
media
|
media
|
||||||
.filter((m) => m.type !== 'image' && m.type !== 'voice' && m.type !== 'video' && m.type !== 'audio' && m.type !== 'gif')
|
.filter((m) => m.type !== 'image' && m.type !== 'voice' && m.type !== 'video' && !isAudioFile(m) && m.type !== 'gif')
|
||||||
.map((m) => {
|
.map((m) => {
|
||||||
const formatSize = (bytes?: number | null) => {
|
const formatSize = (bytes?: number | null) => {
|
||||||
if (!bytes) return "";
|
if (!bytes) return "";
|
||||||
|
|||||||
@@ -224,10 +224,15 @@ export default function MessageInput({ chatId }: MessageInputProps) {
|
|||||||
fileSize: res.size
|
fileSize: res.size
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
let msgType = 'file';
|
||||||
|
if (attachments.every(a => a.type === 'image')) msgType = 'image';
|
||||||
|
else if (attachments.every(a => a.type === 'audio')) msgType = 'audio';
|
||||||
|
else if (attachments.every(a => a.type === 'video')) msgType = 'video';
|
||||||
|
|
||||||
socket.emit('send_message', {
|
socket.emit('send_message', {
|
||||||
chatId,
|
chatId,
|
||||||
content: trimmed || null,
|
content: trimmed || null,
|
||||||
type: attachments.length > 0 ? (attachments.every(a => a.type === 'image') ? 'image' : 'file') : 'text',
|
type: msgType,
|
||||||
attachments: socketAttachments,
|
attachments: socketAttachments,
|
||||||
replyToId: replyTo?.id || null,
|
replyToId: replyTo?.id || null,
|
||||||
quote: replyTo?.quote || null,
|
quote: replyTo?.quote || null,
|
||||||
|
|||||||
Reference in New Issue
Block a user