Files
Hushian/Presentation/HushianWebApp/Pages/FromUserSide/UserCP.razor
mmrbnjd eeb659a0df ...
2025-08-13 18:37:26 +03:30

1725 lines
58 KiB
Plaintext

@page "/UserCP/{CompanyID:int}"
@page "/UserCP/{CompanyID:int}/{ChatID:int?}"
@using Common.Dtos.Company
@using Common.Dtos.Conversation
@using Common.Dtos.Group
@using HushianWebApp.Service
@using HushianWebApp.Services
@using Microsoft.AspNetCore.SignalR.Client;
@using System.Threading;
@inject NavigationManager NavigationManager
@inject ChatService ChatService
@inject ILocalStorageService localStorageService;
@inject AuthService authService;
@inject BaseController baseController;
@inject CompanyService companyService
@inject UserService userService
@inject GroupService groupService
@inject ChatService chatService
@inject IJSRuntime JS
@inject ToastService toastService
@inject HttpClient _Http;
@layout UserPanelLayout
<ConfirmDialog @ref="dialog" />
<div class="container-fluid">
<div class="row" style="height:85vh">
@if (IsEndFirstProcess)
{
@if (IsLogin)
{
<div class="col-md-12 d-flex flex-column" id="B">
<div class="input-group">
@if (LastOpenChat != null)
{
<p type="text" class="form-control fw-bold text-primary" style="border:none;align-self: center;" aria-describedby="basic-addon1">@ExperYou</p>
}
<div class="d-flex gap-2 ms-auto">
@if (LastOpenChat != null)
{
@if (LastOpenChat.status == Common.Enums.ConversationStatus.InProgress)
{
<Button Color="ButtonColor.Danger" Size=ButtonSize.ExtraSmall Outline="true" @onclick="CloseChat" Class="finish-conversation-btn">
<Icon Name="IconName.Escape" Class="me-1" /> اتمام گفتگو
</Button>
}
<Button Color="ButtonColor.Success" Size=ButtonSize.ExtraSmall Outline="true" @onclick="NewChat" Class="new-conversation-btn">
<Icon Name="IconName.ChatDots" Class="me-1" /> گفتگو جدید
</Button>
}
<Button Color="ButtonColor.Secondary" Size=ButtonSize.ExtraSmall Outline="true" @onclick="Logout" Class="logout-btn">
<Icon Name="IconName.BoxArrowRight" Class="me-1" /> خروج
</Button>
</div>
</div>
<!-- B1: Chat area -->
<div class="flex-fill chat-area-container" id="B1">
@if (LastOpenChat != null && LastOpenChat.Responses != null)
{
<div class="chat-container p-3">
@{
bool target = false;
}
@foreach (var msg in LastOpenChat?.Responses)
{
@if (!target && ((!msg.IsRead && msg.Type != Common.Enums.ConversationType.UE) || LastOpenChat.Responses.Last() == msg))
{
target = true;
<div id="target" class="chat-separator">
@if (!msg.IsRead && msg.Type != Common.Enums.ConversationType.UE)
{
<div class="separator-line new-message">
<span class="separator-text">پیام جدید</span>
</div>
}
</div>
}
<div class="d-flex mb-2 @(msg.Type!=Common.Enums.ConversationType.UE ? "justify-content-end" : "justify-content-start")">
<div class="chat-bubble @(msg.Type!=Common.Enums.ConversationType.UE ? "chat-mine": "chat-other")" data-id="@msg.ID">
@if (msg.FileContent != null && msg.FileContent.Length > 0 && !string.IsNullOrWhiteSpace(msg.FileType))
{
@if (msg.FileType.StartsWith("image/"))
{
<a href="@GetImageDataUrl(msg.FileType, msg.FileContent)" download="@GetDownloadFileName(msg.FileName, msg.FileType)" title="دانلود تصویر" style="display:inline-block">
<img src="@GetImageDataUrl(msg.FileType, msg.FileContent)" alt="image" style="max-width: 220px; border-radius: 8px; display: block; cursor: pointer;" />
</a>
}
else if (msg.FileType.StartsWith("audio/"))
{
<div class="audio-message">
<audio controls style="max-width: 250px;">
<source src="@GetAudioDataUrl(msg.FileType, msg.FileContent)" type="@msg.FileType">
مرورگر شما از پخش صدا پشتیبانی نمی‌کند.
</audio>
</div>
}
@if (!string.IsNullOrWhiteSpace(msg.text))
{
<div style="margin-top:6px">@msg.text</div>
}
}
else
{
@msg.text
}
</div>
@if (msg.Type == Common.Enums.ConversationType.UE)
{
if (msg.IsRead)
{
<Icon Style="align-self: self-end;" Name="IconName.CheckAll" Size="IconSize.x5" Color="IconColor.Success" />
}
else
{
<Icon Style="align-self: self-end;" Name="IconName.CheckLg" Size="IconSize.x5" />
}
}
</div>
}
@{
target = false;
}
</div>
}
else
{
<div class="d-flex justify-content-center align-items-center flex-column" style="height: 80%;">
<Spinner Type="SpinnerType.Dots" Color="SpinnerColor.Primary" Visible="@chatloading" />
<p style="margin-top: 15px; font-size: 1.5rem; color: #0d6efd; font-weight: bold; text-shadow: 1px 1px 2px rgba(0,0,0,0.2);">
هوشیان
</p>
@if (CompanyGroups != null && CompanyGroups.Any())
{
<div class="groups-container mt-4">
<h6 class="text-center mb-3 text-muted">انتخاب گروه:</h6>
<div class="groups-grid">
@foreach (var group in CompanyGroups)
{
<div class="group-card @(GroupID == group.ID ? "selected" : "")"
@onclick="() => SelectGroup(group.ID)">
<div class="group-card-content">
@if (group.img == null || group.img.Length == 0)
{
<Icon Name="IconName.People" Class="group-icon" />
}
else
{
<Image src="@GetImageSource(group.img)" class="rounded mx-2" height="50" width="50" alt="Uploaded Image" />
}
<span class="group-name">@group.Name</span>
</div>
</div>
}
</div>
</div>
}
</div>
}
</div>
@if (LastOpenChat == null || (LastOpenChat != null && LastOpenChat.status != Common.Enums.ConversationStatus.Finished && LastOpenChat.Responses != null))
{
<!-- B2: Message input -->
<div class="message-input-container" id="B2">
<div class="input-wrapper">
<input type="text" @bind-value="MsgInput" class="message-input" placeholder="پیام خود را بنویسید..." @onkeydown="HandleKeyDown" />
<InputFile id="chatImageInput" style="display:none" OnChange="OnImageSelected" accept="image/*" />
<Button Color="ButtonColor.Secondary" Size=ButtonSize.Small Outline="true" @onclick="OpenFileDialog" Class="attach-btn" title="افزودن تصویر">
<Icon Name="IconName.Image" />
</Button>
<!-- Audio Recording Button -->
<Button Color="@(IsRecording ? ButtonColor.Danger : ButtonColor.Secondary)"
Size=ButtonSize.Small
Outline="true"
@onclick="ToggleAudioRecording"
class=@($"audio-btn {(IsRecording ? "recording" : "")}")
title="@(IsRecording ? "توقف ضبط" : "ضبط صدا")"
>
@if (IsRecording)
{
<Icon Name="IconName.StopCircle" Class="recording-pulse" />
}
else
{
<Icon Name="IconName.Mic" />
}
</Button>
<Button Color="ButtonColor.Primary" Size=ButtonSize.Small @onclick="OnClickSendMsg" Class="send-btn" title="ارسال">
<Icon Name="IconName.Send" />
</Button>
</div>
<!-- Image Preview -->
@if (SelectedImagePreview != null)
{
<div class="mt-2 d-flex align-items-center gap-2">
<img src="@SelectedImagePreview" alt="preview" style="max-height:60px;border-radius:8px;border:1px solid #e9ecef;" />
<Button Color="ButtonColor.Secondary" Size=ButtonSize.ExtraSmall Outline="true" @onclick="ClearSelectedImage">حذف تصویر</Button>
</div>
}
<!-- Audio Preview -->
@if (RecordedAudioBytes != null)
{
<div class="mt-2 d-flex align-items-center gap-2 audio-preview">
<div class="audio-preview-controls">
<audio controls style="max-width: 250px;">
<source src="@RecordedAudioUrl" type="audio/wav">
</audio>
<div class="audio-preview-info">
<small class="text-muted">@RecordedAudioDuration</small>
</div>
</div>
<Button Color="ButtonColor.Danger" Size=ButtonSize.ExtraSmall Outline="true" @onclick="ClearRecordedAudio">حذف صدا</Button>
</div>
}
<!-- Recording Status -->
@if (IsRecording)
{
<div class="mt-2 recording-status">
<div class="recording-indicator">
<span class="recording-dot"></span>
<span class="recording-text">در حال ضبط... @RecordingTime</span>
</div>
</div>
}
</div>
}
</div>
}
else
{
<div class="d-flex justify-content-center align-items-center" style="height: 100%;">
<div class="login-container p-4 bg-white rounded shadow-sm" style="max-width: 400px; width: 100%;">
<div class="text-center mb-4">
<h4 class="text-primary mb-2">ورود به سیستم</h4>
</div>
<LoginComponent OnMultipleOfThree="EventCallback.Factory.Create(this, Afterlogin)" />
</div>
</div>
}
}
else
{
<div class="d-flex justify-content-center align-items-center" style="height: 100%;">
<div class="text-center">
<Spinner Type="SpinnerType.Dots" Color="SpinnerColor.Primary" />
<p class="mt-3 text-muted">در حال بررسی وضعیت ...</p>
</div>
</div>
}
</div>
</div>
@code {
[Parameter] public int CompanyID { get; set; }
[Parameter] public int? ChatID { get; set; }
private ConfirmDialog dialog = default!;
private HubConnection? hubConnection;
private bool _shouldObserveVisibility = false;
int? GroupID = null;
ReadANDUpdate_CompanyDto? CompanyInfo = new();
Common.Dtos.CurrentUserInfo CurrentUser = new();
List<Read_GroupDto> CompanyGroups = new();
ChatItemDto? LastOpenChat = new();
string MsgInput = string.Empty;
IBrowserFile? SelectedImageFile = null;
byte[]? SelectedImageBytes = null;
string? SelectedImagePreview = null;
// Audio recording properties
bool IsRecording = false;
string RecordingTime = "00:00";
byte[]? RecordedAudioBytes = null;
string? RecordedAudioUrl = null;
string RecordedAudioDuration = "00:00";
private System.Threading.Timer? recordingTimer;
private DateTime recordingStartTime;
bool chatloading = false;
public bool IsLogin { get; set; } = false;
public bool IsEndFirstProcess { get; set; } = false;
string ExperYou
{
get
{
if (CompanyInfo == null) return string.Empty;
string value = $"{CompanyInfo.FullName}";
if (GroupID.HasValue)
{
value += "/" + CompanyGroups.FirstOrDefault(f => f.ID == GroupID.GetValueOrDefault()).Name;
}
if (LastOpenChat != null && LastOpenChat.Responses!=null)
{
var model = LastOpenChat.Responses.OrderBy(o => o.ID).LastOrDefault(l => l.Type != Common.Enums.ConversationType.UE);
if (model!=null && model.Type==Common.Enums.ConversationType.CU && !string.IsNullOrEmpty(CompanyInfo.FullNameManager))
{
value += "/" + CompanyInfo.FullNameManager;
}
else
if (!string.IsNullOrEmpty(LastOpenChat.ExperFullName))
value += "/" + LastOpenChat.ExperFullName;
}
return value;
}
}
// Audio recording methods
private async Task ToggleAudioRecording()
{
if (IsRecording)
{
await StopAudioRecording();
}
else
{
await StartAudioRecording();
}
}
private async Task StartAudioRecording()
{
try
{
var result = await JS.InvokeAsync<bool>("startAudioRecording");
if (result)
{
IsRecording = true;
recordingStartTime = DateTime.Now;
recordingTimer = new System.Threading.Timer(UpdateRecordingTime, null, 0, 1000);
StateHasChanged();
}
else
{
toastService.Notify(new ToastMessage(ToastType.Warning, "خطا در شروع ضبط صدا"));
}
}
catch (Exception ex)
{
toastService.Notify(new ToastMessage(ToastType.Danger, $"خطا در ضبط صدا: {ex.Message}"));
}
}
private async Task StopAudioRecording()
{
try
{
var audioData = await JS.InvokeAsync<string>("stopAudioRecording");
if (!string.IsNullOrEmpty(audioData))
{
// Convert base64 to bytes
var base64Data = audioData.Split(',')[1];
RecordedAudioBytes = Convert.FromBase64String(base64Data);
RecordedAudioUrl = audioData;
RecordedAudioDuration = RecordingTime;
IsRecording = false;
recordingTimer?.Dispose();
recordingTimer = null;
await ClearSelectedImage();
StateHasChanged();
}
}
catch (Exception ex)
{
toastService.Notify(new ToastMessage(ToastType.Danger, $"خطا در توقف ضبط صدا: {ex.Message}"));
}
finally
{
IsRecording = false;
recordingTimer?.Dispose();
recordingTimer = null;
StateHasChanged();
}
}
private void UpdateRecordingTime(object? state)
{
var elapsed = DateTime.Now - recordingStartTime;
RecordingTime = $"{elapsed.Minutes:D2}:{elapsed.Seconds:D2}";
InvokeAsync(StateHasChanged);
}
private Task ClearRecordedAudio()
{
RecordedAudioBytes = null;
RecordedAudioUrl = null;
RecordedAudioDuration = "00:00";
return Task.CompletedTask;
}
private string GetAudioDataUrl(string? fileType, byte[]? content)
=> (string.IsNullOrWhiteSpace(fileType) || content == null || content.Length == 0)
? string.Empty
: $"data:{fileType};base64,{Convert.ToBase64String(content)}";
}
@functions {
async Task OnClickSendMsg()
{
if (!string.IsNullOrEmpty(MsgInput) || SelectedImageFile != null || RecordedAudioBytes != null)
{
if (LastOpenChat != null)
{
Common.Enums.ConversationType type = Common.Enums.ConversationType.UE;
ChatItemResponseDto? model;
if (SelectedImageFile != null)
{
var bytes = SelectedImageBytes ?? Array.Empty<byte>();
model = await chatService.ADDChatResponse(
LastOpenChat.ID,
MsgInput,
type,
SelectedImageFile.Name,
SelectedImageFile.ContentType,
bytes);
}
else if (RecordedAudioBytes != null)
{
// Send audio message
var fileName = $"audio_{DateTimeOffset.Now.ToUnixTimeSeconds()}.wav";
model = await chatService.ADDChatResponse(
LastOpenChat.ID,
MsgInput,
type,
fileName,
"audio/wav",
RecordedAudioBytes);
}
else
{
model = await chatService.ADDChatResponse(LastOpenChat.ID, MsgInput, type);
}
LastOpenChat?.Responses.Add(model);
LastOpenChat.LastText = MsgInput;
}
else
{
//TODO New Chat
var model = await chatService.NewChatFromCurrentUser(new ADD_ConversationDto()
{
CompanyID = CompanyID,
GroupID = GroupID,
Question = MsgInput,
UserID = 0
});
if (model != null)
{
LastOpenChat = model;
}
else toastService.Notify(new ToastMessage(ToastType.Danger, "خطا در گفتگو جدید"));
}
await Task.Yield();
// Scroll to bottom for user's own messages
await JS.InvokeVoidAsync("scrollToBottom", "B1");
MsgInput = string.Empty;
SelectedImageFile = null;
SelectedImageBytes = null;
SelectedImagePreview = null;
// Clear recorded audio after sending
RecordedAudioBytes = null;
RecordedAudioUrl = null;
RecordedAudioDuration = "00:00";
}
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (_shouldObserveVisibility)
{
_shouldObserveVisibility = false;
await JS.InvokeVoidAsync("observeVisibility", DotNetObjectReference.Create(this));
}
}
protected override async Task OnInitializedAsync()
{
await IsOnline();
//-------------hub
var token = await localStorageService.GetItem<string>("U/key");
string AddressHub = _Http.BaseAddress.AbsoluteUri.Replace("api/", "");
hubConnection = new HubConnectionBuilder()
.WithUrl($"{AddressHub}chatNotificationHub", options =>
{
options.AccessTokenProvider = () => Task.FromResult(token);
})
.WithAutomaticReconnect()
.Build();
hubConnection.On<ChatItemResponseDto>("ReceiveNewChatItemFromCompany",async (chatitem) =>
{
if (LastOpenChat.ID == chatitem.ChatItemID)
{
LastOpenChat.Responses.Add(chatitem);
StateHasChanged();
await MarkAsRead(chatitem.ID);
// Scroll to target if exists, otherwise scroll to bottom
await JS.InvokeVoidAsync("scrollToTargetOrBottom");
}
});
//-------------------------------------
hubConnection.On<int>("CheckMarkAsRead", async (chatresponseid) =>
{
if (LastOpenChat.Responses.Any(a=>a.ID==chatresponseid && !a.IsRead && a.Type==Common.Enums.ConversationType.UE))
{
LastOpenChat.Responses.First(a => a.ID == chatresponseid).IsRead = true;
StateHasChanged();
}
});
await hubConnection.StartAsync();
//---------end hub
await base.OnInitializedAsync();
}
async Task IsOnline()
{
var token = await localStorageService.GetItem<string>("U/key");
if (string.IsNullOrEmpty(token))
{
IsLogin = false;
IsEndFirstProcess = true;
}
else
{
await baseController.RemoveToken();
await baseController.SetToken(token);
if (!await authService.IsOnline())
{
await baseController.RemoveToken();
IsLogin = false;
IsEndFirstProcess = true;
}
else
{
IsEndFirstProcess = true;
await Afterlogin();
}
}
}
async Task Afterlogin()
{
IsLogin = true;
CurrentUser = await userService.GetCurrentUserInfo();
await IsCompany();
}
async Task IsCompany()
{
CompanyInfo = await companyService.GetCompany(CompanyID);
if (CompanyInfo != null)
CompanyGroups = await groupService.GetGroupsCompany(CompanyID);
await IsLastChat();
}
async Task IsLastChat()
{
if (CompanyInfo != null)
{
if (ChatID.HasValue) LastOpenChat = await ChatService.Getchat(ChatID.GetValueOrDefault());
else LastOpenChat = LastOpenChat = await ChatService.GetLastOpenChatInCompany(CompanyID);
if (LastOpenChat != null)
{
GroupID = LastOpenChat.GroupID;
// Always set up visibility observation for chat bubbles
_shouldObserveVisibility = true;
StateHasChanged();
// Wait for render to complete
await Task.Delay(200);
// Scroll to target if exists, otherwise scroll to bottom
await JS.InvokeVoidAsync("scrollToTargetOrBottom");
}
}
}
[JSInvokable]
public async Task MarkAsRead(int id)
{
var msg = LastOpenChat.Responses.FirstOrDefault(m => m.ID == id);
if (msg != null && !msg.IsRead && msg.Type != Common.Enums.ConversationType.UE)
{
msg.IsRead = true;
await chatService.MarkAsReadChatItemAsync(id);
// StateHasChanged();
}
await Task.CompletedTask;
}
// Method to handle new messages from other users
public async Task HandleNewMessage()
{
if (LastOpenChat?.Responses != null)
{
var hasUnreadMessages = LastOpenChat.Responses.Any(m => !m.IsRead && m.Type != Common.Enums.ConversationType.UE);
if (hasUnreadMessages)
{
await JS.InvokeVoidAsync("autoScrollToNewMessage");
}
}
}
async Task NewChat()
{
LastOpenChat = null;
}
async Task CloseChat()
{
var options = new ConfirmDialogOptions
{
YesButtonText = "بله",
YesButtonColor = ButtonColor.Success,
NoButtonText = "انصراف",
NoButtonColor = ButtonColor.Danger
};
var confirmation = await dialog.ShowAsync(
title: "پایان دادن به گفتگو",
message1: "اطمینان دارید ؟",
confirmDialogOptions: options);
if (confirmation)
{
if (await chatService.ChatIsFinishFromUser(LastOpenChat.ID))
{
LastOpenChat.status = Common.Enums.ConversationStatus.Finished;
StateHasChanged();
}
}
}
async Task Logout()
{
await baseController.RemoveToken();
await localStorageService.RemoveItem("U/key");
IsLogin = false;
StateHasChanged();
}
async Task SelectGroup(int groupId)
{
GroupID = groupId;
StateHasChanged();
}
private string GetImageSource(byte[] bytes)
=> $"data:image/jpeg;base64,{Convert.ToBase64String(bytes)}";
private async Task HandleKeyDown(KeyboardEventArgs e)
{
if (e.Key == "Enter") await OnClickSendMsg();
}
}
@functions {
private async Task OpenFileDialog()
{
await JS.InvokeVoidAsync("triggerClick", "chatImageInput");
}
private async Task OnImageSelected(InputFileChangeEventArgs e)
{
var file = e.File;
if (file is null)
{
SelectedImageFile = null;
SelectedImageBytes = null;
SelectedImagePreview = null;
return;
}
SelectedImageFile = file;
using var memoryStream = new MemoryStream();
await file.OpenReadStream().CopyToAsync(memoryStream);
SelectedImageBytes = memoryStream.ToArray();
SelectedImagePreview = $"data:{file.ContentType};base64,{Convert.ToBase64String(SelectedImageBytes)}";
await ClearRecordedAudio();
}
private Task ClearSelectedImage()
{
SelectedImageFile = null;
SelectedImageBytes = null;
SelectedImagePreview = null;
return Task.CompletedTask;
}
private static string GetImageDataUrl(string? fileType, byte[]? content)
=> (string.IsNullOrWhiteSpace(fileType) || content == null || content.Length == 0)
? string.Empty
: $"data:{fileType};base64,{Convert.ToBase64String(content)}";
private static string GetDownloadFileName(string? fileName, string? fileType)
{
if (!string.IsNullOrWhiteSpace(fileName)) return fileName;
var ext = "";
if (!string.IsNullOrWhiteSpace(fileType) && fileType.StartsWith("image/"))
{
ext = "." + fileType.Split('/').Last();
}
return $"image_{DateTimeOffset.Now.ToUnixTimeSeconds()}{ext}";
}
}
<style>
.chat-bubble {
padding: 0.5rem 0.75rem;
border-radius: 1rem;
max-width: 75%;
word-wrap: break-word;
white-space: pre-wrap;
}
.chat-mine {
background: linear-gradient(to right, #005eff, #267fff);
color: white;
border-top-left-radius: 0;
}
.chat-other {
background-color: #f1f1f1;
color: #333;
border-top-right-radius: 0;
}
.chat-ai {
background-color: #f1f1f1;
color: #353;
border-top-right-radius: 0;
}
.input-group-text-chat {
display: flex;
align-items: center;
padding: .375rem .75rem;
font-size: 1rem;
font-weight: 400;
line-height: 1.5;
color: var(--bs-body-color);
text-align: center;
white-space: nowrap;
border-radius: var(--bs-border-radius);
}
.login-container {
border: 1px solid #e9ecef;
background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);
}
.login-container h4 {
font-weight: 600;
color: #495057;
}
.login-container p {
font-size: 0.875rem;
line-height: 1.4;
}
/* Improve spacing in the main container */
.container-fluid {
padding: 1rem;
}
/* Better spacing for chat area */
#B1 {
margin-bottom: 1rem;
}
/* Improved input group styling */
.input-group {
margin-bottom: 1rem;
border-radius: 0.5rem;
overflow: hidden;
}
/* Better message input styling */
#B2 {
border-radius: 0.5rem;
background-color: #f8f9fa;
}
/* Enhanced button styling */
.finish-conversation-btn {
border-radius: 20px;
font-weight: 600;
font-size: 0.875rem;
padding: 0.375rem 0.75rem;
transition: all 0.3s ease;
border-width: 2px;
box-shadow: 0 2px 4px rgba(220, 53, 69, 0.2);
}
.finish-conversation-btn:hover {
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(220, 53, 69, 0.3);
border-color: #dc3545;
background-color: #dc3545;
color: white;
}
/* Enhanced button styling */
.new-conversation-btn {
border-radius: 20px;
font-weight: 600;
font-size: 0.875rem;
padding: 0.375rem 0.75rem;
transition: all 0.3s ease;
border-width: 2px;
box-shadow: 0 2px 4px rgba(220, 53, 69, 0.2);
}
.new-conversation-btn:hover {
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(220, 53, 69, 0.3);
border-color: #23caba;
background-color: #23caba;
color: white;
}
.logout-btn {
border-radius: 20px;
font-weight: 600;
font-size: 0.875rem;
padding: 0.375rem 0.75rem;
transition: all 0.3s ease;
border-width: 2px;
box-shadow: 0 2px 4px rgba(108, 117, 125, 0.2);
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
}
.logout-btn:hover {
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(108, 117, 125, 0.3);
border-color: #6c757d;
background: linear-gradient(135deg, #6c757d 0%, #495057 100%);
color: white;
}
/* Improved header layout */
.input-group {
background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%);
border: 1px solid #e9ecef;
border-radius: 15px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.input-group p {
margin: 0;
font-size: 1.1rem;
font-weight: 700;
background: linear-gradient(135deg, #0d6efd 0%, #0b5ed7 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
/* Enhanced chat area styling */
.chat-area-container {
height: 400px;
overflow-y: auto;
background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);
border: 2px solid #e9ecef;
border-radius: 20px;
padding: 1.5rem;
margin: 1rem 0;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05), 0 1px 3px rgba(0, 0, 0, 0.1), inset 0 1px 0 rgba(255, 255, 255, 0.8);
position: relative;
backdrop-filter: blur(10px);
}
.chat-area-container::-webkit-scrollbar {
width: 8px;
}
.chat-area-container::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 10px;
}
.chat-area-container::-webkit-scrollbar-thumb {
background: linear-gradient(135deg, #0d6efd 0%, #0b5ed7 100%);
border-radius: 10px;
transition: all 0.3s ease;
}
.chat-area-container::-webkit-scrollbar-thumb:hover {
background: linear-gradient(135deg, #0b5ed7 0%, #0a4b9e 100%);
}
.chat-container {
background: transparent;
border-radius: 15px;
padding: 0;
}
.chat-container p {
color: #6c757d;
font-size: 0.875rem;
margin: 0.5rem 0;
text-align: center;
font-weight: 500;
}
/* Enhanced chat bubble styling */
.chat-bubble {
padding: 0.75rem 1rem;
border-radius: 18px;
max-width: 75%;
word-wrap: break-word;
white-space: pre-wrap;
font-size: 0.95rem;
line-height: 1.4;
position: relative;
transition: all 0.3s ease;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.chat-mine {
background: linear-gradient(135deg, #005eff 0%, #267fff 100%);
color: white;
border-top-left-radius: 4px;
box-shadow: 0 4px 12px rgba(0, 94, 255, 0.3);
}
.chat-mine:hover {
transform: translateY(-1px);
box-shadow: 0 6px 16px rgba(0, 94, 255, 0.4);
}
.chat-other {
background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%);
color: #333;
border-top-right-radius: 4px;
border: 1px solid #e9ecef;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
.chat-other:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
}
.chat-ai {
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
color: #495057;
border-top-right-radius: 4px;
border: 1px solid #dee2e6;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
/* Message status indicators */
.chat-bubble + i {
margin-left: 0.5rem;
opacity: 0.7;
transition: opacity 0.3s ease;
}
.chat-bubble + i:hover {
opacity: 1;
}
/* Empty state styling */
.chat-area-container .d-flex.justify-content-center {
background: linear-gradient(135deg, rgba(13, 110, 253, 0.05) 0%, rgba(13, 110, 253, 0.02) 100%);
border-radius: 15px;
padding: 2rem;
margin: 1rem;
}
.chat-area-container .d-flex.justify-content-center p {
background: linear-gradient(135deg, #0d6efd 0%, #0b5ed7 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
font-size: 1.75rem;
font-weight: 800;
text-shadow: none;
margin-top: 1rem;
}
/* Enhanced message input styling */
.message-input-container {
margin: 0.5rem 0;
padding: 0.5rem;
background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%);
border: 2px solid #e9ecef;
border-radius: 25px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05), 0 1px 3px rgba(0, 0, 0, 0.1), inset 0 1px 0 rgba(255, 255, 255, 0.8);
transition: all 0.3s ease;
}
.message-input-container:hover {
border-color: #0d6efd;
box-shadow: 0 6px 12px rgba(13, 110, 253, 0.15), 0 2px 4px rgba(0, 0, 0, 0.1), inset 0 1px 0 rgba(255, 255, 255, 0.9);
}
.message-input-container:focus-within {
border-color: #0d6efd;
box-shadow: 0 8px 16px rgba(13, 110, 253, 0.2), 0 2px 4px rgba(0, 0, 0, 0.1), inset 0 1px 0 rgba(255, 255, 255, 0.9);
transform: translateY(-1px);
}
.input-wrapper {
display: flex;
align-items: center;
gap: 0.75rem;
}
.message-input {
flex: 1;
border: none;
background: transparent;
padding: 0.5rem 0.75rem;
font-size: 0.95rem;
color: #495057;
outline: none;
border-radius: 20px;
transition: all 0.3s ease;
}
.message-input::placeholder {
color: #adb5bd;
font-weight: 400;
transition: color 0.3s ease;
}
.message-input:focus::placeholder {
color: #6c757d;
}
.message-input:focus {
background: rgba(13, 110, 253, 0.05);
}
.send-btn {
border-radius: 50%;
width: 38px;
height: 38px;
padding: 0;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #0d6efd 0%, #0b5ed7 100%);
border: none;
box-shadow: 0 4px 12px rgba(13, 110, 253, 0.3);
transition: all 0.3s ease;
color: white;
}
.send-btn:hover {
transform: translateY(-2px) scale(1.05);
box-shadow: 0 6px 16px rgba(13, 110, 253, 0.4);
background: linear-gradient(135deg, #0b5ed7 0%, #0a4b9e 100%);
}
.send-btn:active {
transform: translateY(0) scale(0.95);
box-shadow: 0 2px 8px rgba(13, 110, 253, 0.3);
}
.send-btn i {
font-size: 1.1rem;
transition: transform 0.3s ease;
}
.send-btn:hover i {
transform: scale(1.1);
}
.attach-btn {
border-radius: 50%;
width: 38px;
height: 38px;
padding: 0;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
border: 1px solid #e9ecef;
box-shadow: 0 4px 12px rgba(108, 117, 125, 0.2);
transition: all 0.3s ease;
color: #495057;
}
.attach-btn:hover {
transform: translateY(-2px) scale(1.05);
box-shadow: 0 6px 16px rgba(108, 117, 125, 0.3);
background: linear-gradient(135deg, #e9ecef 0%, #dee2e6 100%);
}
.attach-btn:active {
transform: translateY(0) scale(0.95);
box-shadow: 0 2px 8px rgba(108, 117, 125, 0.2);
}
/* Audio recording button styling */
.audio-btn {
border-radius: 50%;
width: 38px;
height: 38px;
padding: 0;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
border: 1px solid #e9ecef;
box-shadow: 0 4px 12px rgba(108, 117, 125, 0.2);
transition: all 0.3s ease;
color: #495057;
position: relative;
overflow: hidden;
}
.audio-btn:hover {
transform: translateY(-2px) scale(1.05);
box-shadow: 0 6px 16px rgba(108, 117, 125, 0.3);
background: linear-gradient(135deg, #e9ecef 0%, #dee2e6 100%);
}
.audio-btn:active {
transform: translateY(0) scale(0.95);
box-shadow: 0 2px 8px rgba(108, 117, 125, 0.2);
}
.audio-btn.recording {
background: linear-gradient(135deg, #dc3545 0%, #c82333 100%);
border-color: #dc3545;
color: white;
animation: recordingPulse 1.5s ease-in-out infinite;
}
.audio-btn.recording:hover {
background: linear-gradient(135deg, #c82333 0%, #bd2130 100%);
transform: translateY(-2px) scale(1.05);
box-shadow: 0 6px 16px rgba(220, 53, 69, 0.4);
}
.recording-pulse {
animation: iconPulse 1s ease-in-out infinite;
}
@@keyframes recordingPulse {
0%, 100% {
box-shadow: 0 4px 12px rgba(220, 53, 69, 0.3);
}
50% {
box-shadow: 0 4px 20px rgba(220, 53, 69, 0.6), 0 0 30px rgba(220, 53, 69, 0.3);
}
}
@@keyframes iconPulse {
0%, 100% {
transform: scale(1);
}
50% {
transform: scale(1.1);
}
}
/* Audio preview styling */
.audio-preview {
background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);
border: 1px solid #e9ecef;
border-radius: 12px;
padding: 0.75rem;
margin-top: 0.5rem;
}
.audio-preview-controls {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.audio-preview-controls audio {
border-radius: 8px;
background: #f8f9fa;
}
.audio-preview-info {
text-align: center;
color: #6c757d;
font-size: 0.875rem;
}
/* Recording status styling */
.recording-status {
background: linear-gradient(135deg, #fff3cd 0%, #ffeaa7 100%);
border: 1px solid #ffc107;
border-radius: 12px;
padding: 0.75rem;
margin-top: 0.5rem;
animation: recordingStatusPulse 2s ease-in-out infinite;
}
@@keyframes recordingStatusPulse {
0%, 100% {
box-shadow: 0 2px 8px rgba(255, 193, 7, 0.2);
}
50% {
box-shadow: 0 4px 16px rgba(255, 193, 7, 0.4);
}
}
.recording-indicator {
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
}
.recording-dot {
width: 12px;
height: 12px;
background: #dc3545;
border-radius: 50%;
animation: recordingDotPulse 1s ease-in-out infinite;
}
@@keyframes recordingDotPulse {
0%, 100% {
opacity: 1;
transform: scale(1);
}
50% {
opacity: 0.7;
transform: scale(1.2);
}
}
.recording-text {
color: #856404;
font-weight: 600;
font-size: 0.875rem;
}
/* Audio message styling in chat */
.audio-message {
display: flex;
flex-direction: column;
gap: 0.5rem;
align-items: center;
}
.audio-message audio {
border-radius: 8px;
background: #f8f9fa;
border: 1px solid #e9ecef;
}
.audio-info {
text-align: center;
color: #6c757d;
font-size: 0.75rem;
}
/* Responsive design for audio elements */
@@media (max-width: 768px) {
.audio-preview-controls audio {
max-width: 200px;
}
.audio-message audio {
max-width: 200px;
}
}
@@media (max-width: 480px) {
.audio-preview-controls audio {
max-width: 180px;
}
.audio-message audio {
max-width: 180px;
}
.recording-text {
font-size: 0.8rem;
}
}
/* Beautiful chat separator styling */
.chat-separator {
text-align: center;
margin: 1.5rem 0;
position: relative;
}
.separator-line {
display: flex;
align-items: center;
justify-content: center;
position: relative;
margin: 0 1rem;
}
.separator-line::before {
content: '';
position: absolute;
top: 50%;
left: 0;
right: 0;
height: 1px;
background: linear-gradient(90deg, transparent 0%, rgba(13, 110, 253, 0.3) 20%, rgba(13, 110, 253, 0.6) 50%, rgba(13, 110, 253, 0.3) 80%, transparent 100%);
transform: translateY(-50%);
z-index: 1;
}
.separator-text {
background: linear-gradient(135deg, #0d6efd 0%, #0b5ed7 100%);
color: white;
padding: 0.25rem 1rem;
border-radius: 20px;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
box-shadow: 0 2px 8px rgba(13, 110, 253, 0.3);
position: relative;
z-index: 2;
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
animation: separatorPulse 2s ease-in-out infinite;
}
@@keyframes separatorPulse {
0%, 100% {
transform: scale(1);
box-shadow: 0 2px 8px rgba(13, 110, 253, 0.3);
}
50% {
transform: scale(1.05);
box-shadow: 0 4px 12px rgba(13, 110, 253, 0.4);
}
}
.separator-text::before {
content: '';
position: absolute;
top: -2px;
left: -2px;
right: -2px;
bottom: -2px;
background: linear-gradient(135deg, #0d6efd 0%, #0b5ed7 100%);
border-radius: 20px;
z-index: -1;
opacity: 0.3;
filter: blur(4px);
}
/* Hover effect for separator */
.separator-line:hover .separator-text {
transform: scale(1.1);
box-shadow: 0 6px 16px rgba(13, 110, 253, 0.5);
transition: all 0.3s ease;
}
/* Alternative separator style for different themes */
.separator-line.alternative::before {
background: linear-gradient(90deg, transparent 0%, rgba(220, 53, 69, 0.3) 20%, rgba(220, 53, 69, 0.6) 50%, rgba(220, 53, 69, 0.3) 80%, transparent 100%);
}
.separator-line.alternative .separator-text {
background: linear-gradient(135deg, #dc3545 0%, #c82333 100%);
box-shadow: 0 2px 8px rgba(220, 53, 69, 0.3);
}
.separator-line.alternative .separator-text::before {
background: linear-gradient(135deg, #dc3545 0%, #c82333 100%);
}
/* New message separator with different color scheme */
.separator-line.new-message::before {
background: linear-gradient(90deg, transparent 0%, rgba(255, 193, 7, 0.3) 20%, rgba(255, 193, 7, 0.6) 50%, rgba(255, 193, 7, 0.3) 80%, transparent 100%);
}
.separator-line.new-message .separator-text {
background: linear-gradient(135deg, #ffc107 0%, #e0a800 100%);
color: #212529;
box-shadow: 0 2px 8px rgba(255, 193, 7, 0.3);
border: 1px solid rgba(255, 255, 255, 0.3);
}
.separator-line.new-message .separator-text::before {
background: linear-gradient(135deg, #ffc107 0%, #e0a800 100%);
}
@@keyframes newMessagePulse {
0%, 100% {
transform: scale(1);
box-shadow: 0 2px 8px rgba(255, 193, 7, 0.3);
}
50% {
transform: scale(1.05);
box-shadow: 0 4px 12px rgba(255, 193, 7, 0.4);
}
}
.separator-line.new-message .separator-text {
animation: newMessagePulse 2s ease-in-out infinite;
}
.separator-line.new-message:hover .separator-text {
transform: scale(1.1);
box-shadow: 0 6px 16px rgba(255, 193, 7, 0.5);
background: linear-gradient(135deg, #e0a800 0%, #d39e00 100%);
}
/* Group selection cards styling */
.groups-container {
width: 100%;
max-width: 600px;
margin: 0 auto;
}
.groups-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 0.75rem;
padding: 0.75rem;
}
.group-card {
background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%);
border: 2px solid #e9ecef;
border-radius: 12px;
padding: 1rem;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05), 0 1px 3px rgba(0, 0, 0, 0.1);
position: relative;
overflow: hidden;
}
.group-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(135deg, rgba(13, 110, 253, 0.05) 0%, rgba(13, 110, 253, 0.02) 100%);
opacity: 0;
transition: opacity 0.3s ease;
border-radius: 15px;
}
.group-card:hover {
transform: translateY(-3px);
box-shadow: 0 8px 16px rgba(13, 110, 253, 0.15), 0 2px 4px rgba(0, 0, 0, 0.1);
border-color: #0d6efd;
}
.group-card:hover::before {
opacity: 1;
}
.group-card.selected {
background: linear-gradient(135deg, #0d6efd 0%, #0b5ed7 100%);
border-color: #0d6efd;
color: white;
box-shadow: 0 8px 16px rgba(13, 110, 253, 0.3), 0 2px 4px rgba(0, 0, 0, 0.1);
}
.group-card.selected::before {
opacity: 0;
}
.group-card.selected:hover {
background: linear-gradient(135deg, #0b5ed7 0%, #0a4b9e 100%);
transform: translateY(-2px);
}
.group-card-content {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
text-align: center;
position: relative;
z-index: 1;
}
.group-icon {
font-size: 1.5rem;
color: #0d6efd;
transition: all 0.3s ease;
}
.group-card:hover .group-icon {
color: #0b5ed7;
transform: scale(1.1);
}
.group-card.selected .group-icon {
color: white;
}
.group-card.selected:hover .group-icon {
color: rgba(255, 255, 255, 0.9);
}
.group-name {
font-weight: 600;
font-size: 0.875rem;
color: #495057;
transition: all 0.3s ease;
}
.group-card:hover .group-name {
color: #0d6efd;
}
.group-card.selected .group-name {
color: white;
}
.group-card.selected:hover .group-name {
color: rgba(255, 255, 255, 0.9);
}
/* Responsive design for group cards */
@@media (max-width: 768px) {
.groups-grid {
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
gap: 0.5rem;
padding: 0.5rem;
}
.group-card {
padding: 0.75rem;
}
.group-icon {
font-size: 1.25rem;
}
.group-name {
font-size: 0.8rem;
}
}
@@media (max-width: 480px) {
.groups-grid {
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
gap: 0.5rem;
padding: 0.5rem;
}
.group-card {
padding: 0.5rem;
}
.group-icon {
font-size: 1rem;
}
.group-name {
font-size: 0.75rem;
}
}
</style>
<script>
// Audio recording variables
let mediaRecorder = null;
let audioChunks = [];
let audioStream = null;
window.startAudioRecording = async () => {
try {
// Request microphone access
audioStream = await navigator.mediaDevices.getUserMedia({ audio: true });
// Create MediaRecorder
mediaRecorder = new MediaRecorder(audioStream, {
mimeType: 'audio/webm;codecs=opus'
});
audioChunks = [];
// Collect audio data
mediaRecorder.ondataavailable = (event) => {
if (event.data.size > 0) {
audioChunks.push(event.data);
}
};
// Start recording
mediaRecorder.start();
return true;
} catch (error) {
console.error('Error starting audio recording:', error);
return false;
}
};
window.stopAudioRecording = () => {
return new Promise((resolve) => {
if (mediaRecorder && mediaRecorder.state !== 'inactive') {
mediaRecorder.onstop = async () => {
try {
// Create audio blob
const audioBlob = new Blob(audioChunks, { type: 'audio/webm' });
// Convert to base64
const reader = new FileReader();
reader.onloadend = () => {
// Convert webm to wav format (simplified)
const base64Data = reader.result;
resolve(base64Data);
};
reader.readAsDataURL(audioBlob);
// Stop all tracks
if (audioStream) {
audioStream.getTracks().forEach(track => track.stop());
audioStream = null;
}
} catch (error) {
console.error('Error processing audio:', error);
resolve('');
}
};
mediaRecorder.stop();
} else {
resolve('');
}
});
};
window.scrollToBottom = (elementId) => {
const el = document.getElementById(elementId);
if (el) {
el.scrollTo({
top: el.scrollHeight,
behavior: 'smooth'
});
}
};
// Scroll to target element (new message separator)
window.scrollToTarget = () => {
const targetElement = document.getElementById('target');
if (targetElement) {
const chatContainer = document.getElementById('B1');
if (chatContainer) {
// Calculate the position of target element relative to chat container
const targetRect = targetElement.getBoundingClientRect();
const containerRect = chatContainer.getBoundingClientRect();
const relativeTop = targetRect.top - containerRect.top;
// Scroll to show the target element with some padding
const scrollPosition = chatContainer.scrollTop + relativeTop - 100; // 100px padding
chatContainer.scrollTo({
top: scrollPosition,
behavior: 'smooth'
});
}
} else {
// If no target element exists, scroll to bottom smoothly
window.scrollToBottom('B1');
}
};
// Observe visibility of chat bubbles using existing scroll-visibility.js approach
window.observeVisibility = (dotNetHelper) => {
const elements = document.querySelectorAll(".chat-bubble[data-id]");
const observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const id = entry.target.getAttribute("data-id");
dotNetHelper.invokeMethodAsync("MarkAsRead", parseInt(id));
observer.unobserve(entry.target); // Don't observe again
}
});
}, {
threshold: 0.6 // 60% of the message must be visible
});
elements.forEach(el => observer.observe(el));
};
// Auto scroll to target when new messages arrive
window.autoScrollToNewMessage = () => {
setTimeout(() => {
window.scrollToTarget();
}, 100); // Small delay to ensure DOM is updated
};
// Check if target exists and scroll accordingly
window.scrollToTargetOrBottom = () => {
const targetElement = document.getElementById('target');
if (targetElement && targetElement.style.display !== 'none') {
window.scrollToTarget();
} else {
window.scrollToBottom('B1');
}
};
// Trigger click on hidden input by id
window.triggerClick = (elementId) => {
const el = document.getElementById(elementId);
if (el) {
el.click();
}
};
</script>