Messenger Online / Offline mekanizması
This commit is contained in:
parent
af726c530a
commit
771ea95e55
11 changed files with 137 additions and 52 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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; } = [];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -66,14 +66,20 @@ public class OrgChartAppService : PlatformAppService
|
|||
topPositionByDepartment[department.Id] = selectedTop;
|
||||
}
|
||||
|
||||
var nodes = departments.Select(d => new OrgChartNodeDto
|
||||
var nodes = departments.Select(d =>
|
||||
{
|
||||
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 =>
|
||||
topPositionByDepartment.TryGetValue(d.Id, out var topPosition) &&
|
||||
topPosition != null &&
|
||||
u.GetJobPositionId() == topPosition.Id)
|
||||
.Select(u => new OrgChartUserDto
|
||||
{
|
||||
|
|
@ -81,8 +87,10 @@ public class OrgChartAppService : PlatformAppService
|
|||
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();
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
let active = true
|
||||
|
||||
const loadContacts = () => {
|
||||
getMessengerContacts(filter)
|
||||
.then((response) => setContacts(response.data))
|
||||
.catch(() => setContacts([]))
|
||||
.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
|
||||
? 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>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ export interface MessengerContactDto {
|
|||
fullName: string
|
||||
email?: string
|
||||
isActive: boolean
|
||||
isOnline: boolean
|
||||
}
|
||||
|
||||
export interface MessengerAttachmentDto {
|
||||
|
|
|
|||
|
|
@ -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[]
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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 */}
|
||||
|
|
|
|||
Loading…
Reference in a new issue