...
This commit is contained in:
@@ -289,11 +289,67 @@
|
||||
<!-- B2: Message input -->
|
||||
<div class="message-input-container" id="B2">
|
||||
<div class="input-wrapper">
|
||||
<input type="text" @bind-value="MsgInput" class="message-input" placeholder="پیام خود را بنویسید..." />
|
||||
<Button Color="ButtonColor.Primary" Size=ButtonSize.Small @onclick="OnClickSendMsg" Class="send-btn">
|
||||
<input type="text" @bind-value="MsgInput" class="message-input" placeholder="پیام خود را بنویسید..." />
|
||||
<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>
|
||||
}
|
||||
|
||||
@@ -318,6 +374,19 @@
|
||||
/////////////
|
||||
ChatItemDto? ChatCurrent { get; set; } = null;
|
||||
string MsgInput { get; set; }
|
||||
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;
|
||||
string SelectedChatUserName = "مهدی ربیع نژاد";
|
||||
private bool _shouldObserveVisibility = false;
|
||||
@@ -548,9 +617,145 @@
|
||||
var seconds = durationSeconds % 60;
|
||||
return $"{minutes:D2}:{seconds:D2}";
|
||||
}
|
||||
|
||||
// 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 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
<style>
|
||||
.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);
|
||||
}
|
||||
|
||||
/* Enhanced Sidebar Styling */
|
||||
.sidebar-container {
|
||||
background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%);
|
||||
@@ -1441,9 +1646,273 @@
|
||||
background: linear-gradient(135deg, #6c757d 0%, #495057 100%);
|
||||
color: white;
|
||||
}
|
||||
/* 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;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
// Trigger click on hidden input by id
|
||||
window.triggerClick = (elementId) => {
|
||||
const el = document.getElementById(elementId);
|
||||
if (el) {
|
||||
el.click();
|
||||
}
|
||||
};
|
||||
// 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.getWindowSize = () => {
|
||||
return {
|
||||
width: window.innerWidth,
|
||||
|
Reference in New Issue
Block a user