Messenger Hub
This commit is contained in:
parent
0c202ece24
commit
7632c9e8a0
10 changed files with 915 additions and 2 deletions
|
|
@ -0,0 +1,59 @@
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using Volo.Abp.Content;
|
||||||
|
|
||||||
|
namespace Sozsoft.Platform.Messenger;
|
||||||
|
|
||||||
|
public class MessengerContactDto
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
public Guid? TenantId { get; set; }
|
||||||
|
public string UserName { get; set; } = string.Empty;
|
||||||
|
public string? Name { get; set; }
|
||||||
|
public string? Surname { get; set; }
|
||||||
|
public string FullName { get; set; } = string.Empty;
|
||||||
|
public string? Email { get; set; }
|
||||||
|
public bool IsActive { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class MessengerAttachmentDto
|
||||||
|
{
|
||||||
|
public string FileName { get; set; } = string.Empty;
|
||||||
|
public string SavedFileName { get; set; } = string.Empty;
|
||||||
|
public string FileType { get; set; } = "application/octet-stream";
|
||||||
|
public long FileSize { get; set; }
|
||||||
|
public string Url { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class MessengerUploadAttachmentInput
|
||||||
|
{
|
||||||
|
[Required]
|
||||||
|
public IRemoteStreamContent File { get; set; } = default!;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class MessengerSendMessageDto
|
||||||
|
{
|
||||||
|
[Required]
|
||||||
|
public List<Guid> RecipientIds { get; set; } = new();
|
||||||
|
|
||||||
|
[StringLength(4096)]
|
||||||
|
public string? Text { get; set; }
|
||||||
|
|
||||||
|
public List<MessengerAttachmentDto> Attachments { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class MessengerMessageDto
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
public Guid? TenantId { get; set; }
|
||||||
|
public Guid SenderId { get; set; }
|
||||||
|
public string SenderUserName { get; set; } = string.Empty;
|
||||||
|
public string SenderName { get; set; } = string.Empty;
|
||||||
|
public List<Guid> RecipientIds { get; set; } = new();
|
||||||
|
public string? Text { get; set; }
|
||||||
|
public List<MessengerAttachmentDto> Attachments { get; set; } = new();
|
||||||
|
public DateTime SentAt { get; set; }
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,114 @@
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Sozsoft.Platform.BlobStoring;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Volo.Abp;
|
||||||
|
using Volo.Abp.Application.Services;
|
||||||
|
using Volo.Abp.Domain.Repositories;
|
||||||
|
using Volo.Abp.Identity;
|
||||||
|
|
||||||
|
namespace Sozsoft.Platform.Messenger;
|
||||||
|
|
||||||
|
[Authorize]
|
||||||
|
public class MessengerAppService : ApplicationService
|
||||||
|
{
|
||||||
|
private readonly IRepository<IdentityUser, Guid> _userRepository;
|
||||||
|
private readonly BlobManager _blobManager;
|
||||||
|
private readonly IConfiguration _configuration;
|
||||||
|
|
||||||
|
public MessengerAppService(
|
||||||
|
IRepository<IdentityUser, Guid> userRepository,
|
||||||
|
BlobManager blobManager,
|
||||||
|
IConfiguration configuration)
|
||||||
|
{
|
||||||
|
_userRepository = userRepository;
|
||||||
|
_blobManager = blobManager;
|
||||||
|
_configuration = configuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<MessengerContactDto>> GetContactsAsync(string? filter = null)
|
||||||
|
{
|
||||||
|
var query = await _userRepository.GetQueryableAsync();
|
||||||
|
var currentUserId = CurrentUser.Id;
|
||||||
|
var currentTenantId = CurrentTenant.Id;
|
||||||
|
|
||||||
|
query = query.Where(user =>
|
||||||
|
user.IsActive &&
|
||||||
|
user.Id != currentUserId &&
|
||||||
|
user.TenantId == currentTenantId);
|
||||||
|
|
||||||
|
if (!filter.IsNullOrWhiteSpace())
|
||||||
|
{
|
||||||
|
var normalizedFilter = filter!.Trim().ToLower();
|
||||||
|
query = query.Where(user =>
|
||||||
|
user.UserName.ToLower().Contains(normalizedFilter) ||
|
||||||
|
(user.Name != null && user.Name.ToLower().Contains(normalizedFilter)) ||
|
||||||
|
(user.Surname != null && user.Surname.ToLower().Contains(normalizedFilter)) ||
|
||||||
|
(user.Email != null && user.Email.ToLower().Contains(normalizedFilter)));
|
||||||
|
}
|
||||||
|
|
||||||
|
var users = await AsyncExecuter.ToListAsync(
|
||||||
|
query
|
||||||
|
.OrderBy(user => user.Name ?? user.UserName)
|
||||||
|
.ThenBy(user => user.Surname)
|
||||||
|
.Take(100)
|
||||||
|
);
|
||||||
|
|
||||||
|
return users.Select(user =>
|
||||||
|
{
|
||||||
|
var fullName = $"{user.Name} {user.Surname}".Trim();
|
||||||
|
return new MessengerContactDto
|
||||||
|
{
|
||||||
|
Id = user.Id,
|
||||||
|
TenantId = user.TenantId,
|
||||||
|
UserName = user.UserName,
|
||||||
|
Name = user.Name,
|
||||||
|
Surname = user.Surname,
|
||||||
|
FullName = fullName.IsNullOrWhiteSpace() ? user.UserName : fullName,
|
||||||
|
Email = user.Email,
|
||||||
|
IsActive = user.IsActive
|
||||||
|
};
|
||||||
|
}).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("api/app/messenger/upload-attachment")]
|
||||||
|
public async Task<MessengerAttachmentDto> UploadAttachmentAsync([FromForm] MessengerUploadAttachmentInput input)
|
||||||
|
{
|
||||||
|
if (input.File is null || input.File.ContentLength is null or <= 0)
|
||||||
|
{
|
||||||
|
throw new UserFriendlyException("Dosya seçilmelidir.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var originalFileName = Path.GetFileName(input.File.FileName);
|
||||||
|
if (originalFileName.IsNullOrWhiteSpace())
|
||||||
|
{
|
||||||
|
throw new UserFriendlyException("Dosya adı geçersiz.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var savedFileName = $"{Guid.NewGuid():N}_{originalFileName}";
|
||||||
|
|
||||||
|
await using var stream = input.File.GetStream();
|
||||||
|
await _blobManager.SaveAsync(BlobContainerNames.Messenger, savedFileName, stream, true);
|
||||||
|
|
||||||
|
var tenantPart = CurrentTenant.Id.HasValue
|
||||||
|
? $"tenants/{CurrentTenant.Id.Value}"
|
||||||
|
: "host";
|
||||||
|
var baseUrl = _configuration["App:CdnUrl"]?.TrimEnd('/') ?? string.Empty;
|
||||||
|
|
||||||
|
return new MessengerAttachmentDto
|
||||||
|
{
|
||||||
|
FileName = originalFileName,
|
||||||
|
SavedFileName = savedFileName,
|
||||||
|
FileType = input.File.ContentType ?? "application/octet-stream",
|
||||||
|
FileSize = input.File.ContentLength ?? 0,
|
||||||
|
Url = $"{baseUrl}/{tenantPart}/{BlobContainerNames.Messenger}/{savedFileName}"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
119
api/src/Sozsoft.Platform.Application/Messenger/MessengerHub.cs
Normal file
119
api/src/Sozsoft.Platform.Application/Messenger/MessengerHub.cs
Normal file
|
|
@ -0,0 +1,119 @@
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.SignalR;
|
||||||
|
using Volo.Abp.Domain.Repositories;
|
||||||
|
using Volo.Abp.Guids;
|
||||||
|
using Volo.Abp.Identity;
|
||||||
|
using Volo.Abp.MultiTenancy;
|
||||||
|
using Volo.Abp.Users;
|
||||||
|
|
||||||
|
namespace Sozsoft.Platform.Messenger;
|
||||||
|
|
||||||
|
[Authorize]
|
||||||
|
public class MessengerHub : Hub
|
||||||
|
{
|
||||||
|
private readonly ICurrentTenant _currentTenant;
|
||||||
|
private readonly ICurrentUser _currentUser;
|
||||||
|
private readonly IGuidGenerator _guidGenerator;
|
||||||
|
private readonly IRepository<IdentityUser, Guid> _userRepository;
|
||||||
|
|
||||||
|
public MessengerHub(
|
||||||
|
ICurrentTenant currentTenant,
|
||||||
|
ICurrentUser currentUser,
|
||||||
|
IGuidGenerator guidGenerator,
|
||||||
|
IRepository<IdentityUser, Guid> userRepository)
|
||||||
|
{
|
||||||
|
_currentTenant = currentTenant;
|
||||||
|
_currentUser = currentUser;
|
||||||
|
_guidGenerator = guidGenerator;
|
||||||
|
_userRepository = userRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task OnConnectedAsync()
|
||||||
|
{
|
||||||
|
if (_currentUser.Id.HasValue)
|
||||||
|
{
|
||||||
|
await Groups.AddToGroupAsync(Context.ConnectionId, UserGroupName(_currentUser.Id.Value));
|
||||||
|
await Groups.AddToGroupAsync(Context.ConnectionId, TenantGroupName());
|
||||||
|
}
|
||||||
|
|
||||||
|
await base.OnConnectedAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task OnDisconnectedAsync(Exception? exception)
|
||||||
|
{
|
||||||
|
if (_currentUser.Id.HasValue)
|
||||||
|
{
|
||||||
|
await Groups.RemoveFromGroupAsync(Context.ConnectionId, UserGroupName(_currentUser.Id.Value));
|
||||||
|
await Groups.RemoveFromGroupAsync(Context.ConnectionId, TenantGroupName());
|
||||||
|
}
|
||||||
|
|
||||||
|
await base.OnDisconnectedAsync(exception);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SendMessage(MessengerSendMessageDto input)
|
||||||
|
{
|
||||||
|
if (!_currentUser.Id.HasValue)
|
||||||
|
{
|
||||||
|
throw new HubException("Kullanıcı oturumu bulunamadı.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var recipientIds = input.RecipientIds
|
||||||
|
.Where(id => id != Guid.Empty)
|
||||||
|
.Distinct()
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (recipientIds.Count == 0)
|
||||||
|
{
|
||||||
|
throw new HubException("En az bir alıcı seçilmelidir.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.Text.IsNullOrWhiteSpace() && input.Attachments.Count == 0)
|
||||||
|
{
|
||||||
|
throw new HubException("Mesaj veya dosya gönderilmelidir.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var tenantId = _currentTenant.Id;
|
||||||
|
var validRecipients = await _userRepository.GetListAsync(user =>
|
||||||
|
recipientIds.Contains(user.Id) &&
|
||||||
|
user.TenantId == tenantId &&
|
||||||
|
user.IsActive);
|
||||||
|
|
||||||
|
if (validRecipients.Count == 0)
|
||||||
|
{
|
||||||
|
throw new HubException("Bu tenant içinde geçerli alıcı bulunamadı.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var senderName = _currentUser.Name ?? _currentUser.UserName ?? "Kullanıcı";
|
||||||
|
var message = new MessengerMessageDto
|
||||||
|
{
|
||||||
|
Id = _guidGenerator.Create(),
|
||||||
|
TenantId = tenantId,
|
||||||
|
SenderId = _currentUser.Id.Value,
|
||||||
|
SenderUserName = _currentUser.UserName ?? string.Empty,
|
||||||
|
SenderName = senderName,
|
||||||
|
RecipientIds = validRecipients.Select(user => user.Id).ToList(),
|
||||||
|
Text = input.Text?.Trim(),
|
||||||
|
Attachments = input.Attachments,
|
||||||
|
SentAt = DateTime.UtcNow
|
||||||
|
};
|
||||||
|
|
||||||
|
var targetGroups = message.RecipientIds
|
||||||
|
.Append(message.SenderId)
|
||||||
|
.Distinct()
|
||||||
|
.Select(UserGroupName)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
await Clients.Groups(targetGroups).SendAsync("MessengerMessageReceived", message);
|
||||||
|
}
|
||||||
|
|
||||||
|
private string TenantKey => _currentTenant.Id?.ToString("N") ?? "host";
|
||||||
|
|
||||||
|
private string TenantGroupName() => $"messenger:tenant:{TenantKey}";
|
||||||
|
|
||||||
|
private string UserGroupName(Guid userId) => $"messenger:user:{TenantKey}:{userId:N}";
|
||||||
|
}
|
||||||
|
|
@ -5,6 +5,7 @@ public static class BlobContainerNames
|
||||||
public const string Intranet = "intranet";
|
public const string Intranet = "intranet";
|
||||||
public const string Avatar = "avatar";
|
public const string Avatar = "avatar";
|
||||||
public const string Import = "import";
|
public const string Import = "import";
|
||||||
|
public const string Messenger = "messenger";
|
||||||
public const string Note = "note";
|
public const string Note = "note";
|
||||||
public const string Backup = "backup";
|
public const string Backup = "backup";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,7 @@ using DevExpress.AspNetCore;
|
||||||
using DevExpress.AspNetCore.Reporting;
|
using DevExpress.AspNetCore.Reporting;
|
||||||
using DevExpress.XtraReports.Web.Extensions;
|
using DevExpress.XtraReports.Web.Extensions;
|
||||||
using Sozsoft.Platform.ReportServices;
|
using Sozsoft.Platform.ReportServices;
|
||||||
|
using Sozsoft.Platform.Messenger;
|
||||||
|
|
||||||
namespace Sozsoft.Platform;
|
namespace Sozsoft.Platform;
|
||||||
|
|
||||||
|
|
@ -483,6 +484,17 @@ public class PlatformHttpApiHostModule : AbpModule
|
||||||
app.MapAbpStaticAssets();
|
app.MapAbpStaticAssets();
|
||||||
app.UseRouting();
|
app.UseRouting();
|
||||||
app.UseCors();
|
app.UseCors();
|
||||||
|
app.Use(async (context, next) =>
|
||||||
|
{
|
||||||
|
if (context.Request.Path.StartsWithSegments("/messengerhub") &&
|
||||||
|
context.Request.Query.TryGetValue("access_token", out var accessToken) &&
|
||||||
|
!string.IsNullOrWhiteSpace(accessToken.ToString()))
|
||||||
|
{
|
||||||
|
context.Request.Headers.Authorization = $"Bearer {accessToken}";
|
||||||
|
}
|
||||||
|
|
||||||
|
await next();
|
||||||
|
});
|
||||||
app.UseAuthentication();
|
app.UseAuthentication();
|
||||||
app.UseAbpOpenIddictValidation();
|
app.UseAbpOpenIddictValidation();
|
||||||
app.UseMiddleware<CaptchaMiddleware>();
|
app.UseMiddleware<CaptchaMiddleware>();
|
||||||
|
|
@ -522,7 +534,10 @@ public class PlatformHttpApiHostModule : AbpModule
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
app.UseConfiguredEndpoints();
|
app.UseConfiguredEndpoints(endpoints =>
|
||||||
|
{
|
||||||
|
endpoints.MapHub<MessengerHub>("/messengerhub");
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public override Task OnPostApplicationInitializationAsync(ApplicationInitializationContext context)
|
public override Task OnPostApplicationInitializationAsync(ApplicationInitializationContext context)
|
||||||
|
|
|
||||||
|
|
@ -73,6 +73,7 @@ server {
|
||||||
server {
|
server {
|
||||||
listen 443 ssl http2;
|
listen 443 ssl http2;
|
||||||
server_name api.sozsoft.com;
|
server_name api.sozsoft.com;
|
||||||
|
client_max_body_size 512M;
|
||||||
ssl_certificate /etc/letsencrypt/live/api.sozsoft.com/fullchain.pem; # managed by Certbot
|
ssl_certificate /etc/letsencrypt/live/api.sozsoft.com/fullchain.pem; # managed by Certbot
|
||||||
ssl_trusted_certificate /etc/ssl/sozsoft.com/chain1.pem;
|
ssl_trusted_certificate /etc/ssl/sozsoft.com/chain1.pem;
|
||||||
ssl_certificate_key /etc/letsencrypt/live/api.sozsoft.com/privkey.pem; # managed by Certbot
|
ssl_certificate_key /etc/letsencrypt/live/api.sozsoft.com/privkey.pem; # managed by Certbot
|
||||||
|
|
@ -88,6 +89,18 @@ server {
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
proxy_set_header Connection "upgrade";
|
proxy_set_header Connection "upgrade";
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header __tenant $http___tenant;
|
||||||
|
proxy_read_timeout 3600;
|
||||||
|
proxy_send_timeout 3600;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /messengerhub {
|
||||||
|
proxy_pass http://127.0.0.1:8080/messengerhub;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection "upgrade";
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header __tenant $http___tenant;
|
||||||
proxy_read_timeout 3600;
|
proxy_read_timeout 3600;
|
||||||
proxy_send_timeout 3600;
|
proxy_send_timeout 3600;
|
||||||
}
|
}
|
||||||
|
|
@ -197,4 +210,4 @@ server {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import useLocale from '@/utils/hooks/useLocale'
|
||||||
import { useDynamicRoutes } from '@/routes/dynamicRoutesContext'
|
import { useDynamicRoutes } from '@/routes/dynamicRoutesContext'
|
||||||
import { useLocation } from 'react-router-dom'
|
import { useLocation } from 'react-router-dom'
|
||||||
import { hasSubdomain } from '@/utils/subdomain'
|
import { hasSubdomain } from '@/utils/subdomain'
|
||||||
|
import MessengerWidget from '../template/MessengerWidget'
|
||||||
|
|
||||||
export type LayoutType =
|
export type LayoutType =
|
||||||
| typeof LAYOUT_TYPE_CLASSIC
|
| typeof LAYOUT_TYPE_CLASSIC
|
||||||
|
|
@ -102,6 +103,7 @@ const Layout = () => {
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<AppLayout />
|
<AppLayout />
|
||||||
|
{authenticated && currentPath.includes('/admin') && <MessengerWidget />}
|
||||||
</Suspense>
|
</Suspense>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
442
ui/src/components/template/MessengerWidget.tsx
Normal file
442
ui/src/components/template/MessengerWidget.tsx
Normal file
|
|
@ -0,0 +1,442 @@
|
||||||
|
import Avatar from '@/components/ui/Avatar'
|
||||||
|
import Button from '@/components/ui/Button'
|
||||||
|
import Input from '@/components/ui/Input'
|
||||||
|
import Notification from '@/components/ui/Notification'
|
||||||
|
import Tooltip from '@/components/ui/Tooltip'
|
||||||
|
import { toast } from '@/components/ui'
|
||||||
|
import { AVATAR_URL } from '@/constants/app.constant'
|
||||||
|
import {
|
||||||
|
getMessengerContacts,
|
||||||
|
MessengerAttachmentDto,
|
||||||
|
MessengerContactDto,
|
||||||
|
uploadMessengerAttachment,
|
||||||
|
} from '@/services/messenger.service'
|
||||||
|
import { messengerSignalR, MessengerMessageDto } from '@/services/messenger.signalr'
|
||||||
|
import { useStoreState } from '@/store'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
import EmojiPicker, { EmojiClickData } from 'emoji-picker-react'
|
||||||
|
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||||
|
import { FaArrowLeft, FaPaperclip, FaRegSmile, FaSearch, FaTimes, FaTrash } from 'react-icons/fa'
|
||||||
|
import { IoCheckmarkDone, IoChatbubbleEllipsesOutline, IoSend } from 'react-icons/io5'
|
||||||
|
|
||||||
|
const MAX_UPLOAD_SIZE = 10 * 1024 * 1024
|
||||||
|
|
||||||
|
const formatFileSize = (size: number) => {
|
||||||
|
if (size < 1024 * 1024) return `${Math.max(1, Math.round(size / 1024))} KB`
|
||||||
|
return `${(size / (1024 * 1024)).toFixed(1)} MB`
|
||||||
|
}
|
||||||
|
|
||||||
|
const MessengerWidget = () => {
|
||||||
|
const auth = useStoreState((state) => state.auth)
|
||||||
|
const tenantId = auth.user.tenantId || auth.tenant?.tenantId
|
||||||
|
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||||
|
const messagesEndRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
|
const [open, setOpen] = useState(false)
|
||||||
|
const [contacts, setContacts] = useState<MessengerContactDto[]>([])
|
||||||
|
const [selectedIds, setSelectedIds] = useState<string[]>([])
|
||||||
|
const [messages, setMessages] = useState<MessengerMessageDto[]>([])
|
||||||
|
const [text, setText] = useState('')
|
||||||
|
const [filter, setFilter] = useState('')
|
||||||
|
const [attachments, setAttachments] = useState<MessengerAttachmentDto[]>([])
|
||||||
|
const [connected, setConnected] = useState(false)
|
||||||
|
const [uploading, setUploading] = useState(false)
|
||||||
|
const [showEmoji, setShowEmoji] = useState(false)
|
||||||
|
const [multiSelect, setMultiSelect] = useState(false)
|
||||||
|
const [unread, setUnread] = useState(0)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!auth.session.signedIn) return
|
||||||
|
|
||||||
|
const unsubscribeMessage = messengerSignalR.onMessage((message) => {
|
||||||
|
setMessages((prev) => (prev.some((item) => item.id === message.id) ? prev : [...prev, message]))
|
||||||
|
if (!open && message.senderId !== auth.user.id) {
|
||||||
|
setUnread((count) => count + 1)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const unsubscribeState = messengerSignalR.onStateChange(setConnected)
|
||||||
|
messengerSignalR.start()
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unsubscribeMessage()
|
||||||
|
unsubscribeState()
|
||||||
|
}
|
||||||
|
}, [auth.session.signedIn, auth.user.id, open])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!auth.session.signedIn) {
|
||||||
|
messengerSignalR.stop()
|
||||||
|
}
|
||||||
|
}, [auth.session.signedIn])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) return
|
||||||
|
setUnread(0)
|
||||||
|
getMessengerContacts(filter)
|
||||||
|
.then((response) => setContacts(response.data))
|
||||||
|
.catch(() => setContacts([]))
|
||||||
|
}, [filter, open])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
|
||||||
|
}, [messages, open])
|
||||||
|
|
||||||
|
const selectedContacts = useMemo(
|
||||||
|
() => contacts.filter((contact) => selectedIds.includes(contact.id)),
|
||||||
|
[contacts, selectedIds],
|
||||||
|
)
|
||||||
|
|
||||||
|
const visibleMessages = useMemo(() => {
|
||||||
|
if (selectedIds.length === 0) return messages.slice(-30)
|
||||||
|
|
||||||
|
return messages.filter((message) => {
|
||||||
|
const isOwn = message.senderId === auth.user.id
|
||||||
|
const participants = new Set([message.senderId, ...message.recipientIds])
|
||||||
|
return selectedIds.some((id) => participants.has(id)) && (isOwn || selectedIds.includes(message.senderId))
|
||||||
|
})
|
||||||
|
}, [auth.user.id, messages, selectedIds])
|
||||||
|
|
||||||
|
const toggleContact = (id: string) => {
|
||||||
|
if (!multiSelect) {
|
||||||
|
setSelectedIds([id])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setSelectedIds((prev) => (prev.includes(id) ? prev.filter((item) => item !== id) : [...prev, id]))
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleMultiSelect = () => {
|
||||||
|
setMultiSelect((value) => {
|
||||||
|
if (value) {
|
||||||
|
setSelectedIds((prev) => prev.slice(0, 1))
|
||||||
|
}
|
||||||
|
return !value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleFiles = async (files: FileList | null) => {
|
||||||
|
if (!files?.length) return
|
||||||
|
|
||||||
|
const nextFiles = Array.from(files)
|
||||||
|
const tooLarge = nextFiles.find((file) => file.size > MAX_UPLOAD_SIZE)
|
||||||
|
if (tooLarge) {
|
||||||
|
toast.push(<Notification title="Dosya 10 MB sınırını aşamaz" type="warning" />, {
|
||||||
|
placement: 'top-end',
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setUploading(true)
|
||||||
|
try {
|
||||||
|
const uploaded = await Promise.all(
|
||||||
|
nextFiles.map((file) => uploadMessengerAttachment(file).then((response) => response.data)),
|
||||||
|
)
|
||||||
|
setAttachments((prev) => [...prev, ...uploaded])
|
||||||
|
} catch {
|
||||||
|
toast.push(<Notification title="Dosya yüklenemedi" type="danger" />, { placement: 'top-end' })
|
||||||
|
} finally {
|
||||||
|
setUploading(false)
|
||||||
|
if (fileInputRef.current) fileInputRef.current.value = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const sendMessage = async () => {
|
||||||
|
const trimmedText = text.trim()
|
||||||
|
if (selectedIds.length === 0 || (!trimmedText && attachments.length === 0)) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
await messengerSignalR.sendMessage({
|
||||||
|
recipientIds: selectedIds,
|
||||||
|
text: trimmedText,
|
||||||
|
attachments,
|
||||||
|
})
|
||||||
|
setText('')
|
||||||
|
setAttachments([])
|
||||||
|
setShowEmoji(false)
|
||||||
|
} catch {
|
||||||
|
toast.push(<Notification title="Mesaj gönderilemedi" type="danger" />, { placement: 'top-end' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onEmojiClick = (emoji: EmojiClickData) => {
|
||||||
|
setText((current) => `${current}${emoji.emoji}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!auth.session.signedIn) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="pointer-events-none fixed bottom-5 right-5 z-50 h-14 w-14">
|
||||||
|
{open && (
|
||||||
|
<div className="pointer-events-auto fixed inset-x-2 bottom-20 top-3 overflow-hidden rounded-lg border border-gray-200 bg-white shadow-2xl dark:border-gray-700 dark:bg-gray-800 sm:absolute sm:inset-auto sm:bottom-16 sm:right-0 sm:h-[min(70vh,640px)] sm:w-[min(78vw,800px)]">
|
||||||
|
<div className="flex h-full min-h-0">
|
||||||
|
<aside
|
||||||
|
className={`min-h-0 w-full shrink-0 flex-col border-gray-200 bg-gray-50 dark:border-gray-700 dark:bg-gray-900 sm:flex sm:w-72 sm:border-r ${
|
||||||
|
selectedIds.length > 0 ? 'hidden' : 'flex'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex h-16 items-center justify-between border-b border-gray-200 px-4 dark:border-gray-700">
|
||||||
|
<div className="flex min-w-0 items-center gap-3">
|
||||||
|
<Avatar size={36} shape="circle" src={auth.user.avatar || AVATAR_URL(auth.user.id, tenantId)} />
|
||||||
|
<div className="min-w-0">
|
||||||
|
<div className="truncate text-sm font-semibold text-gray-900 dark:text-gray-100">
|
||||||
|
{auth.user.name || auth.user.userName || 'Messenger'}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1 text-xs text-gray-500">
|
||||||
|
<span className={`h-2 w-2 rounded-full ${connected ? 'bg-emerald-500' : 'bg-gray-400'}`} />
|
||||||
|
{connected ? 'Bağlı' : 'Bağlanıyor'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button size="xs" variant="plain" icon={<FaTimes />} onClick={() => setOpen(false)} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2 p-3">
|
||||||
|
<Input
|
||||||
|
size="sm"
|
||||||
|
value={filter}
|
||||||
|
prefix={<FaSearch />}
|
||||||
|
placeholder="Kişi ara"
|
||||||
|
onChange={(event) => setFilter(event.target.value)}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
variant={multiSelect ? 'twoTone' : 'default'}
|
||||||
|
block
|
||||||
|
onClick={toggleMultiSelect}
|
||||||
|
>
|
||||||
|
{multiSelect ? 'Çoklu seçim açık' : 'Çoklu seçim'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="min-h-0 flex-1 overflow-y-auto px-2 pb-2">
|
||||||
|
{contacts.map((contact) => {
|
||||||
|
const active = selectedIds.includes(contact.id)
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={contact.id}
|
||||||
|
type="button"
|
||||||
|
onClick={() => toggleContact(contact.id)}
|
||||||
|
className={`mb-1 flex w-full items-center gap-3 rounded-md px-2 py-2 text-left transition ${
|
||||||
|
active
|
||||||
|
? 'bg-emerald-50 text-emerald-700 dark:bg-emerald-500/15 dark:text-emerald-100'
|
||||||
|
: 'hover:bg-white dark:hover:bg-gray-800'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Avatar size={36} shape="circle" src={AVATAR_URL(contact.id, contact.tenantId || tenantId)} />
|
||||||
|
<span className="min-w-0 flex-1">
|
||||||
|
<span className="block truncate text-sm font-medium">{contact.fullName}</span>
|
||||||
|
<span className="block truncate text-xs text-gray-500">{contact.email || contact.userName}</span>
|
||||||
|
</span>
|
||||||
|
{active && <IoCheckmarkDone className="shrink-0 text-lg" />}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<main
|
||||||
|
className={`min-w-0 flex-1 flex-col ${
|
||||||
|
selectedIds.length === 0 ? 'hidden sm:flex' : 'flex'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex h-16 items-center justify-between border-b border-gray-200 px-4 dark:border-gray-700">
|
||||||
|
<div className="flex min-w-0 items-center gap-3">
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
variant="plain"
|
||||||
|
icon={<FaArrowLeft />}
|
||||||
|
className="shrink-0 sm:hidden"
|
||||||
|
onClick={() => setSelectedIds([])}
|
||||||
|
/>
|
||||||
|
{selectedContacts.length === 1 && (
|
||||||
|
<Avatar
|
||||||
|
size={36}
|
||||||
|
shape="circle"
|
||||||
|
src={AVATAR_URL(selectedContacts[0].id, selectedContacts[0].tenantId || tenantId)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{selectedContacts.length > 1 && (
|
||||||
|
<Avatar size={36} shape="circle" className="bg-emerald-100 text-emerald-700">
|
||||||
|
{selectedContacts.length}
|
||||||
|
</Avatar>
|
||||||
|
)}
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<div className="truncate text-sm font-semibold text-gray-900 dark:text-gray-100">
|
||||||
|
{selectedContacts.length === 1
|
||||||
|
? selectedContacts[0].fullName
|
||||||
|
: selectedContacts.length > 1
|
||||||
|
? `${selectedContacts.length} kişi seçildi`
|
||||||
|
: 'Sohbet'}
|
||||||
|
</div>
|
||||||
|
<div className="truncate text-xs text-gray-500">
|
||||||
|
{selectedContacts.length === 1
|
||||||
|
? selectedContacts[0].email || selectedContacts[0].userName
|
||||||
|
: selectedContacts.length > 1
|
||||||
|
? 'Toplu mesaj'
|
||||||
|
: 'Mesaj göndermek için kişi seçin'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1 overflow-y-auto bg-white px-4 py-3 dark:bg-gray-800">
|
||||||
|
{visibleMessages.length === 0 && (
|
||||||
|
<div className="flex h-full items-center justify-center text-sm text-gray-400">
|
||||||
|
Henüz mesaj yok
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{visibleMessages.map((message) => {
|
||||||
|
const own = message.senderId === auth.user.id
|
||||||
|
return (
|
||||||
|
<div key={message.id} className={`mb-3 flex ${own ? 'justify-end' : 'justify-start'}`}>
|
||||||
|
<div
|
||||||
|
className={`max-w-[76%] rounded-lg px-3 py-2 shadow-sm ${
|
||||||
|
own
|
||||||
|
? 'bg-emerald-600 text-white'
|
||||||
|
: 'bg-gray-100 text-gray-900 dark:bg-gray-700 dark:text-gray-100'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{!own && <div className="mb-1 text-xs font-semibold">{message.senderName}</div>}
|
||||||
|
{message.text && <div className="whitespace-pre-wrap text-sm leading-5">{message.text}</div>}
|
||||||
|
{message.attachments.length > 0 && (
|
||||||
|
<div className="mt-2 space-y-1">
|
||||||
|
{message.attachments.map((file) => (
|
||||||
|
<a
|
||||||
|
key={file.savedFileName}
|
||||||
|
href={file.url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
className={`block rounded-md px-2 py-1 text-xs ${
|
||||||
|
own ? 'bg-emerald-700 text-white' : 'bg-white text-gray-700 dark:bg-gray-800 dark:text-gray-100'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span className="block truncate">{file.fileName}</span>
|
||||||
|
<span className="opacity-75">{formatFileSize(file.fileSize)}</span>
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className={`mt-1 text-[11px] ${own ? 'text-emerald-100' : 'text-gray-500'}`}>
|
||||||
|
{dayjs(message.sentAt).format('HH:mm')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
<div ref={messagesEndRef} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{selectedIds.length > 0 && attachments.length > 0 && (
|
||||||
|
<div className="flex flex-wrap gap-2 border-t border-gray-200 px-4 py-2 dark:border-gray-700">
|
||||||
|
{attachments.map((file) => (
|
||||||
|
<span
|
||||||
|
key={file.savedFileName}
|
||||||
|
className="inline-flex max-w-[220px] items-center gap-2 rounded-md bg-gray-100 px-2 py-1 text-xs dark:bg-gray-700"
|
||||||
|
>
|
||||||
|
<span className="truncate">{file.fileName}</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() =>
|
||||||
|
setAttachments((prev) =>
|
||||||
|
prev.filter((item) => item.savedFileName !== file.savedFileName),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<FaTrash />
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{selectedIds.length > 0 && (
|
||||||
|
<div className="relative border-t border-gray-200 p-3 dark:border-gray-700">
|
||||||
|
{showEmoji && (
|
||||||
|
<div className="absolute bottom-16 left-2 z-10 sm:left-10">
|
||||||
|
<EmojiPicker
|
||||||
|
onEmojiClick={onEmojiClick}
|
||||||
|
width="min(320px, calc(100vw - 32px))"
|
||||||
|
height={380}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="flex min-h-[52px] items-center gap-2 rounded-md border border-gray-300 bg-white px-2 py-1.5 focus-within:border-emerald-500 focus-within:ring-1 focus-within:ring-emerald-500 dark:border-gray-600 dark:bg-gray-800">
|
||||||
|
<input
|
||||||
|
ref={fileInputRef}
|
||||||
|
type="file"
|
||||||
|
multiple
|
||||||
|
className="hidden"
|
||||||
|
onChange={(event) => handleFiles(event.target.files)}
|
||||||
|
/>
|
||||||
|
<Tooltip title="Dosya ekle">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
size="sm"
|
||||||
|
variant="plain"
|
||||||
|
loading={uploading}
|
||||||
|
icon={<FaPaperclip />}
|
||||||
|
className="shrink-0"
|
||||||
|
onClick={() => fileInputRef.current?.click()}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip title="Emoji">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
size="sm"
|
||||||
|
variant="plain"
|
||||||
|
icon={<FaRegSmile />}
|
||||||
|
className="shrink-0"
|
||||||
|
onClick={() => setShowEmoji((value) => !value)}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<textarea
|
||||||
|
value={text}
|
||||||
|
placeholder="Mesaj yazın"
|
||||||
|
rows={1}
|
||||||
|
autoFocus
|
||||||
|
className="min-h-[36px] flex-1 resize-none border-0 bg-transparent px-2 py-2 text-sm text-gray-900 outline-none placeholder:text-gray-400 focus:ring-0 dark:text-gray-100"
|
||||||
|
onChange={(event) => setText(event.target.value)}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (event.key === 'Enter' && !event.shiftKey) {
|
||||||
|
event.preventDefault()
|
||||||
|
sendMessage()
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
size="sm"
|
||||||
|
variant="solid"
|
||||||
|
icon={<IoSend />}
|
||||||
|
className="shrink-0"
|
||||||
|
disabled={!text.trim() && attachments.length === 0}
|
||||||
|
onClick={sendMessage}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Tooltip title="Messenger">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setOpen((value) => !value)}
|
||||||
|
className="pointer-events-auto relative flex h-12 w-12 items-center justify-center rounded-full bg-emerald-600 text-2xl text-white shadow-xl transition hover:bg-emerald-700"
|
||||||
|
>
|
||||||
|
<IoChatbubbleEllipsesOutline />
|
||||||
|
{unread > 0 && (
|
||||||
|
<span className="absolute min-w-[20px] rounded-full bg-red-500 px-1.5 py-0.5 text-xs font-semibold text-white">
|
||||||
|
{unread > 99 ? '99+' : unread}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MessengerWidget
|
||||||
38
ui/src/services/messenger.service.ts
Normal file
38
ui/src/services/messenger.service.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
import apiService from './api.service'
|
||||||
|
|
||||||
|
export interface MessengerContactDto {
|
||||||
|
id: string
|
||||||
|
tenantId?: string
|
||||||
|
userName: string
|
||||||
|
name?: string
|
||||||
|
surname?: string
|
||||||
|
fullName: string
|
||||||
|
email?: string
|
||||||
|
isActive: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MessengerAttachmentDto {
|
||||||
|
fileName: string
|
||||||
|
savedFileName: string
|
||||||
|
fileType: string
|
||||||
|
fileSize: number
|
||||||
|
url: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getMessengerContacts = (filter?: string) =>
|
||||||
|
apiService.fetchData<MessengerContactDto[]>({
|
||||||
|
method: 'GET',
|
||||||
|
url: '/api/app/messenger/contacts',
|
||||||
|
params: filter ? { filter } : undefined,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const uploadMessengerAttachment = (file: File) => {
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append('file', file)
|
||||||
|
|
||||||
|
return apiService.fetchData<MessengerAttachmentDto>({
|
||||||
|
method: 'POST',
|
||||||
|
url: '/api/app/messenger/upload-attachment',
|
||||||
|
data: formData,
|
||||||
|
})
|
||||||
|
}
|
||||||
110
ui/src/services/messenger.signalr.ts
Normal file
110
ui/src/services/messenger.signalr.ts
Normal file
|
|
@ -0,0 +1,110 @@
|
||||||
|
import { store } from '@/store/store'
|
||||||
|
import * as signalR from '@microsoft/signalr'
|
||||||
|
import { MessengerAttachmentDto } from './messenger.service'
|
||||||
|
|
||||||
|
export interface MessengerMessageDto {
|
||||||
|
id: string
|
||||||
|
tenantId?: string
|
||||||
|
senderId: string
|
||||||
|
senderUserName: string
|
||||||
|
senderName: string
|
||||||
|
recipientIds: string[]
|
||||||
|
text?: string
|
||||||
|
attachments: MessengerAttachmentDto[]
|
||||||
|
sentAt: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MessengerSendMessageDto {
|
||||||
|
recipientIds: string[]
|
||||||
|
text?: string
|
||||||
|
attachments: MessengerAttachmentDto[]
|
||||||
|
}
|
||||||
|
|
||||||
|
type MessageHandler = (message: MessengerMessageDto) => void
|
||||||
|
type StateHandler = (connected: boolean) => void
|
||||||
|
|
||||||
|
class MessengerSignalRService {
|
||||||
|
private connection?: signalR.HubConnection
|
||||||
|
private messageHandlers = new Set<MessageHandler>()
|
||||||
|
private stateHandlers = new Set<StateHandler>()
|
||||||
|
|
||||||
|
private createConnection() {
|
||||||
|
const { auth } = store.getState()
|
||||||
|
|
||||||
|
this.connection = new signalR.HubConnectionBuilder()
|
||||||
|
.withUrl(`${import.meta.env.VITE_API_URL}/messengerhub`, {
|
||||||
|
accessTokenFactory: () => store.getState().auth.session.token || auth.session.token || '',
|
||||||
|
})
|
||||||
|
.withAutomaticReconnect()
|
||||||
|
.configureLogging(signalR.LogLevel.Warning)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
this.connection.on('MessengerMessageReceived', (message: MessengerMessageDto) => {
|
||||||
|
this.messageHandlers.forEach((handler) => handler(message))
|
||||||
|
})
|
||||||
|
|
||||||
|
this.connection.onreconnected(() => this.emitState(true))
|
||||||
|
this.connection.onreconnecting(() => this.emitState(false))
|
||||||
|
this.connection.onclose(() => this.emitState(false))
|
||||||
|
}
|
||||||
|
|
||||||
|
async start() {
|
||||||
|
if (!store.getState().auth.session.signedIn) return
|
||||||
|
|
||||||
|
if (!this.connection) {
|
||||||
|
this.createConnection()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.connection?.state === signalR.HubConnectionState.Connected) {
|
||||||
|
this.emitState(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.connection?.state === signalR.HubConnectionState.Connecting) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.connection?.start()
|
||||||
|
this.emitState(true)
|
||||||
|
} catch {
|
||||||
|
this.emitState(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async stop() {
|
||||||
|
if (!this.connection) return
|
||||||
|
|
||||||
|
await this.connection.stop()
|
||||||
|
this.connection = undefined
|
||||||
|
this.emitState(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendMessage(input: MessengerSendMessageDto) {
|
||||||
|
await this.start()
|
||||||
|
if (this.connection?.state !== signalR.HubConnectionState.Connected) {
|
||||||
|
throw new Error('Messenger bağlantısı yok')
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.connection.invoke('SendMessage', input)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMessage(handler: MessageHandler) {
|
||||||
|
this.messageHandlers.add(handler)
|
||||||
|
return () => this.messageHandlers.delete(handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
onStateChange(handler: StateHandler) {
|
||||||
|
this.stateHandlers.add(handler)
|
||||||
|
handler(this.getConnectionState())
|
||||||
|
return () => this.stateHandlers.delete(handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
getConnectionState() {
|
||||||
|
return this.connection?.state === signalR.HubConnectionState.Connected
|
||||||
|
}
|
||||||
|
|
||||||
|
private emitState(connected: boolean) {
|
||||||
|
this.stateHandlers.forEach((handler) => handler(connected))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const messengerSignalR = new MessengerSignalRService()
|
||||||
Loading…
Reference in a new issue