Messenger Online / Offline mekanizması

This commit is contained in:
Sedat Öztürk 2026-06-13 21:50:39 +03:00
parent af726c530a
commit 771ea95e55
11 changed files with 137 additions and 52 deletions

View file

@ -18,6 +18,7 @@ public class MessengerContactDto
public string FullName { get; set; } = string.Empty;
public string? Email { get; set; }
public bool IsActive { get; set; }
public bool IsOnline { get; set; }
}
public class MessengerAttachmentDto

View file

@ -9,6 +9,7 @@ public class OrgChartUserDto
public string FullName { get; set; }
public string Email { get; set; }
public string UserName { get; set; }
public string PhoneNumber { get; set; }
}
public class OrgChartNodeDto
@ -21,5 +22,8 @@ public class OrgChartNodeDto
public Guid? DepartmentId { get; set; }
public string DepartmentName { get; set; }
public Guid? JobPositionId { get; set; }
public string JobPositionName { get; set; }
public List<OrgChartUserDto> Users { get; set; } = [];
}

View file

@ -26,6 +26,7 @@ public class MessengerAppService : ApplicationService
private readonly IRepository<IdentityUser, Guid> _userRepository;
private readonly IRepository<MessengerConversation, Guid> _conversationRepository;
private readonly IRepository<MessengerConversationMessage, Guid> _messageRepository;
private readonly IIdentitySessionRepository _identitySessionRepository;
private readonly BlobManager _blobManager;
private readonly IConfiguration _configuration;
@ -33,12 +34,14 @@ public class MessengerAppService : ApplicationService
IRepository<IdentityUser, Guid> userRepository,
IRepository<MessengerConversation, Guid> conversationRepository,
IRepository<MessengerConversationMessage, Guid> messageRepository,
IIdentitySessionRepository identitySessionRepository,
BlobManager blobManager,
IConfiguration configuration)
{
_userRepository = userRepository;
_conversationRepository = conversationRepository;
_messageRepository = messageRepository;
_identitySessionRepository = identitySessionRepository;
_blobManager = blobManager;
_configuration = configuration;
}
@ -71,6 +74,8 @@ public class MessengerAppService : ApplicationService
.Take(100)
);
var onlineUserIds = await GetOnlineUserIdsAsync(users.Select(user => user.Id));
return users.Select(user =>
{
var fullName = $"{user.Name} {user.Surname}".Trim();
@ -83,7 +88,8 @@ public class MessengerAppService : ApplicationService
Surname = user.Surname,
FullName = fullName.IsNullOrWhiteSpace() ? user.UserName : fullName,
Email = user.Email,
IsActive = user.IsActive
IsActive = user.IsActive,
IsOnline = onlineUserIds.Contains(user.Id)
};
}).ToList();
}
@ -238,6 +244,12 @@ public class MessengerAppService : ApplicationService
throw new UserFriendlyException("Bu tenant icinde gecersiz alici var.");
}
var onlineRecipientIds = await GetOnlineUserIdsAsync(recipientIds);
if (onlineRecipientIds.Count != recipientIds.Count)
{
throw new UserFriendlyException("Sadece online kullanicilara mesaj gonderebilirsiniz.");
}
MessengerConversation? conversation = null;
if (input.ConversationId.HasValue)
{
@ -406,6 +418,25 @@ public class MessengerAppService : ApplicationService
user.IsActive);
}
private async Task<HashSet<Guid>> GetOnlineUserIdsAsync(IEnumerable<Guid> userIds)
{
var normalizedUserIds = userIds
.Where(id => id != Guid.Empty)
.Distinct()
.ToList();
if (normalizedUserIds.Count == 0)
{
return new HashSet<Guid>();
}
var sessions = await _identitySessionRepository.GetListAsync();
return sessions
.Where(session => normalizedUserIds.Contains(session.UserId))
.Select(session => session.UserId)
.ToHashSet();
}
private async Task<MessengerConversation?> FindConversationByParticipantKeyAsync(string participantKey)
{
var query = await _conversationRepository.GetQueryableAsync();

View file

@ -66,23 +66,31 @@ public class OrgChartAppService : PlatformAppService
topPositionByDepartment[department.Id] = selectedTop;
}
var nodes = departments.Select(d => new OrgChartNodeDto
var nodes = departments.Select(d =>
{
Id = d.Id,
Name = d.Name,
ParentId = d.ParentId,
Users = users
.Where(u =>
topPositionByDepartment.TryGetValue(d.Id, out var topPosition) &&
u.GetJobPositionId() == topPosition.Id)
.Select(u => new OrgChartUserDto
{
Id = u.Id,
FullName = u.GetFullName(),
Email = u.Email,
UserName = u.UserName,
})
.ToList(),
topPositionByDepartment.TryGetValue(d.Id, out var topPosition);
return new OrgChartNodeDto
{
Id = d.Id,
Name = d.Name,
ParentId = d.ParentId,
JobPositionId = topPosition?.Id,
JobPositionName = topPosition?.Name,
Users = users
.Where(u =>
topPosition != null &&
u.GetJobPositionId() == topPosition.Id)
.Select(u => new OrgChartUserDto
{
Id = u.Id,
FullName = u.GetFullName(),
Email = u.Email,
UserName = u.UserName,
PhoneNumber = u.PhoneNumber
})
.ToList(),
};
}).ToList();
return nodes;
@ -104,6 +112,8 @@ public class OrgChartAppService : PlatformAppService
ParentId = jp.ParentId,
DepartmentId = jp.DepartmentId,
DepartmentName = deptDict.TryGetValue(jp.DepartmentId, out var name) ? name : null,
JobPositionId = jp.Id,
JobPositionName = jp.Name,
Users = users
.Where(u => u.GetJobPositionId() == jp.Id)
.Select(u => new OrgChartUserDto
@ -112,6 +122,7 @@ public class OrgChartAppService : PlatformAppService
FullName = u.GetFullName(),
Email = u.Email,
UserName = u.UserName,
PhoneNumber = u.PhoneNumber
})
.ToList(),
}).ToList();

View file

@ -1439,12 +1439,6 @@
"DepartmentName": "Yönetim",
"Name": "Genel Müdür"
},
{
"Id": "a2b3c4d5-e6f7-4890-ab12-cd34ef56ab78",
"DepartmentName": "Yönetim",
"Name": "Genel Müdür Yardımcısı",
"ParentName": "Genel Müdür"
},
{
"Id": "b7c8d9e0-f1a2-4b3c-8d9e-0f1a2b3c4d5e",
"DepartmentName": "Muhasebe",

View file

@ -31,6 +31,7 @@ import {
import { IoCheckmarkDone, IoChatbubbleEllipsesOutline, IoSend } from 'react-icons/io5'
const MAX_UPLOAD_SIZE = 10 * 1024 * 1024
const CONTACT_REFRESH_INTERVAL = 30000
const formatFileSize = (size: number) => {
if (size < 1024 * 1024) return `${Math.max(1, Math.round(size / 1024))} KB`
@ -99,9 +100,25 @@ const MessengerWidget = () => {
useEffect(() => {
if (!open) return
getMessengerContacts(filter)
.then((response) => setContacts(response.data))
.catch(() => setContacts([]))
let active = true
const loadContacts = () => {
getMessengerContacts(filter)
.then((response) => {
if (active) setContacts(response.data)
})
.catch(() => {
if (active) setContacts([])
})
}
loadContacts()
const timer = window.setInterval(loadContacts, CONTACT_REFRESH_INTERVAL)
return () => {
active = false
window.clearInterval(timer)
}
}, [filter, open])
useEffect(() => {
@ -135,6 +152,9 @@ const MessengerWidget = () => {
[contacts, selectedIds],
)
const canSendToSelectedContacts =
selectedContacts.length > 0 && selectedContacts.every((contact) => contact.isOnline)
const visibleMessages = useMemo(() => {
if (selectedIds.length === 0) return []
@ -146,6 +166,14 @@ const MessengerWidget = () => {
}, [auth.user.id, messages, selectedIds])
const toggleContact = (id: string) => {
const contact = contacts.find((item) => item.id === id)
if (!contact?.isOnline) {
toast.push(<Notification title="Offline kullanıcılara mesaj gönderilemez" type="warning" />, {
placement: 'top-end',
})
return
}
setUnreadByContact((current) => {
if (!current[id]) return current
@ -210,6 +238,12 @@ const MessengerWidget = () => {
const sendMessage = async () => {
const trimmedText = text.trim()
if (selectedIds.length === 0 || (!trimmedText && attachments.length === 0)) return
if (!canSendToSelectedContacts) {
toast.push(<Notification title="Sadece online kullanıcılara mesaj gönderebilirsiniz" type="warning" />, {
placement: 'top-end',
})
return
}
try {
await messengerSignalR.sendMessage({
@ -311,16 +345,31 @@ const MessengerWidget = () => {
key={contact.id}
type="button"
onClick={() => toggleContact(contact.id)}
disabled={!contact.isOnline}
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'
: contact.isOnline
? 'hover:bg-white dark:hover:bg-gray-800'
: 'cursor-not-allowed opacity-60'
}`}
>
<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
className={`mt-1 inline-flex items-center gap-1 text-xs font-medium ${
contact.isOnline ? 'text-emerald-600' : 'text-red-500'
}`}
>
<span
className={`h-2 w-2 rounded-full ${
contact.isOnline ? 'bg-emerald-500' : 'bg-red-500'
}`}
/>
{contact.isOnline ? 'Online' : 'Offline'}
</span>
</span>
{contactUnread > 0 && (
<span className="shrink-0 rounded-full bg-red-500 px-2 py-0.5 text-xs font-semibold text-white">
@ -370,9 +419,13 @@ const MessengerWidget = () => {
</div>
<div className="truncate text-xs text-gray-500">
{selectedContacts.length === 1
? selectedContacts[0].email || selectedContacts[0].userName
? `${selectedContacts[0].isOnline ? 'Online' : 'Offline'} - ${
selectedContacts[0].email || selectedContacts[0].userName
}`
: selectedContacts.length > 1
? 'Toplu mesaj'
? canSendToSelectedContacts
? 'Toplu mesaj'
: 'Offline kullanici secili'
: 'Mesaj göndermek için kişi seçin'}
</div>
</div>
@ -518,6 +571,7 @@ const MessengerWidget = () => {
loading={uploading}
icon={<FaPaperclip />}
className="shrink-0"
disabled={!canSendToSelectedContacts}
onClick={() => fileInputRef.current?.click()}
/>
</Tooltip>
@ -537,6 +591,7 @@ const MessengerWidget = () => {
placeholder="Mesaj yazın"
rows={1}
autoFocus
disabled={!canSendToSelectedContacts}
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) => {
@ -552,7 +607,7 @@ const MessengerWidget = () => {
variant="solid"
icon={<IoSend />}
className="shrink-0"
disabled={!text.trim() && attachments.length === 0}
disabled={!canSendToSelectedContacts || (!text.trim() && attachments.length === 0)}
onClick={sendMessage}
/>
</div>

View file

@ -9,6 +9,7 @@ export interface MessengerContactDto {
fullName: string
email?: string
isActive: boolean
isOnline: boolean
}
export interface MessengerAttachmentDto {

View file

@ -5,6 +5,7 @@ export interface OrgChartUserDto {
fullName: string
email: string
userName: string
phoneNumber?: string
}
export interface OrgChartNodeDto {
@ -13,6 +14,8 @@ export interface OrgChartNodeDto {
parentId?: string
departmentId?: string
departmentName?: string
jobPositionId?: string
jobPositionName?: string
users: OrgChartUserDto[]
}

View file

@ -342,12 +342,12 @@ function buildEdges(tree: TreeNode[]): EdgeLink[] {
function UserAvatar({
user,
tenantId,
title,
jobPosition,
department,
}: {
user: OrgChartUserDto
tenantId?: string
title: string
jobPosition?: string
department?: string
}) {
const [showUserCard, setShowUserCard] = useState(false)
@ -381,8 +381,9 @@ function UserAvatar({
user={{
id: user.id,
name: user.fullName || user.userName,
title,
title: jobPosition || '',
email: user.email,
phoneNumber: user.phoneNumber,
department,
tenantId: tenantId ?? '',
}}
@ -518,16 +519,7 @@ function OrgChartNode({
)}
</div>
{/* Department label for job positions */}
{mode === 'jobPosition' && node.departmentName && (
<div className="px-3 pt-2">
<span
className={`inline-block text-xs px-2 py-0.5 rounded-full font-medium ${badgeBg}`}
>
{node.departmentName}
</span>
</div>
)}
{/* Users section */}
{showUsers && (
@ -539,7 +531,7 @@ function OrgChartNode({
<UserAvatar
user={user}
tenantId={tenantId}
title={mode === 'jobPosition' ? node.name : node.departmentName || node.name}
jobPosition={node.jobPositionName || (mode === 'jobPosition' ? node.name : undefined)}
department={mode === 'jobPosition' ? node.departmentName : node.name}
/>
<span data-user-name="" className="text-xs text-slate-600 truncate">

View file

@ -386,6 +386,7 @@ const PostItem: React.FC<PostItemProps> = ({ post, onLike, onComment, onDelete,
id: comment.user?.id || '',
name: comment.user?.fullName || '',
title: comment.user?.jobPositions?.[0]?.name || '',
phoneNumber: comment.user.phoneNumber,
tenantId: comment.user?.tenantId || '',
}}
position="bottom"

View file

@ -12,7 +12,6 @@ interface UserProfileCardProps {
email?: string
phoneNumber?: string
department?: string
location?: string
tenantId: string
}
position?: 'top' | 'bottom'
@ -76,13 +75,6 @@ const UserProfileCard: React.FC<UserProfileCardProps> = ({ user, position = 'bot
<span className="text-gray-700 dark:text-gray-300">{user.department}</span>
</div>
)}
{user.location && (
<div className="flex items-center gap-2 text-sm">
<FaMapMarkerAlt className="w-4 h-4 text-gray-400 flex-shrink-0" />
<span className="text-gray-700 dark:text-gray-300">{user.location}</span>
</div>
)}
</div>
{/* Arrow indicator */}