feat: enhance privacy email viewer, dynamic pagination, and cinematic loading states
This commit is contained in:
@@ -21,6 +21,10 @@ class Mailbox extends Component
|
||||
|
||||
public $search = '';
|
||||
|
||||
public $viewMode = 'text'; // text | html
|
||||
|
||||
public $allowRemoteContent = false;
|
||||
|
||||
public $page = 1;
|
||||
|
||||
public $totalPages = 5;
|
||||
@@ -38,16 +42,17 @@ class Mailbox extends Component
|
||||
|
||||
public function getEmailsProperty()
|
||||
{
|
||||
// Mock emails based on mailbox ID for demonstration
|
||||
$emails = [
|
||||
1 => [
|
||||
// Mock emails based on mailbox ID
|
||||
$baseEmails = [
|
||||
1 => [ // Inbox
|
||||
[
|
||||
'id' => 1,
|
||||
'from_name' => 'GitHub Security',
|
||||
'from_email' => 'noreply@github.com',
|
||||
'subject' => '[GitHub] A new personal access token was created',
|
||||
'preview' => 'A new personal access token (classic) was recently added to your account.',
|
||||
'content' => '<p>Hi @idevakk,</p><p>A new personal access token (classic) was recently added to your account IDEVAKK.</p><p>If this was you, you can safely ignore this email.</p><p>If this was not you, please visit https://github.com/settings/tokens to revoke the token.</p>',
|
||||
'body_html' => '<p>Hi @idevakk,</p><p>A new personal access token (classic) was recently added to your account IDEVAKK.</p><p>If this was you, you can safely ignore this email.</p><p>If this was not you, please visit https://github.com/settings/tokens to revoke the token.</p>',
|
||||
'body_text' => "Hi @idevakk,\n\nA new personal access token (classic) was recently added to your account IDEVAKK.\n\nIf this was you, you can safely ignore this email.\n\nIf this was not you, please visit https://github.com/settings/tokens to revoke the token.",
|
||||
'time' => '10:24 AM',
|
||||
'unread' => true,
|
||||
'flagged' => true,
|
||||
@@ -59,112 +64,131 @@ class Mailbox extends Component
|
||||
'from_email' => 'updates@linear.app',
|
||||
'subject' => 'New issue assigned: [UI-124] Fix sidebar overflow',
|
||||
'preview' => 'You have been assigned to a new issue in the UI project. Please review the details...',
|
||||
'content' => '<p>Hello,</p><p>You have been assigned to <strong>[UI-124] Fix sidebar overflow in mobile view</strong>.</p><p>Priority: High</p><p>Project: Imail Revamp</p><p>View details at https://linear.app/imail/issue/UI-124</p>',
|
||||
'body_html' => '<p>Hello,</p><p>You have been assigned to <strong>[UI-124] Fix sidebar overflow in mobile view</strong>.</p><p>Priority: High</p><p>Project: Imail Revamp</p><p>View details at https://linear.app/imail/issue/UI-124</p>',
|
||||
'body_text' => "Hello,\n\nYou have been assigned to [UI-124] Fix sidebar overflow in mobile view.\n\nPriority: High\nProject: Imail Revamp\n\nView details at https://linear.app/imail/issue/UI-124",
|
||||
'time' => '11:45 AM',
|
||||
'unread' => true,
|
||||
'flagged' => false,
|
||||
'attachments' => [],
|
||||
],
|
||||
[
|
||||
'id' => 102,
|
||||
'from_name' => 'Canva',
|
||||
'from_email' => 'design@canva.com',
|
||||
'subject' => 'Your design "Imail Presentation" is ready',
|
||||
'preview' => 'Collaborate with your team on your latest design for the Imail product launch.',
|
||||
'content' => '<p>Hey there!</p><p>Your team is waiting for your feedback on the <strong>Imail Presentation</strong> design.</p><p>Check the latest comments and approve the final version.</p>',
|
||||
'time' => '9:12 AM',
|
||||
'unread' => false,
|
||||
'id' => 104,
|
||||
'from_name' => 'Unsplash Updates',
|
||||
'from_email' => 'hello@unsplash.com',
|
||||
'subject' => 'Featured Photos: Cinematic Landscapes',
|
||||
'preview' => 'Check out this week\'s curated collection of cinematic landscape photography...',
|
||||
'body_html' => '<p>Hello Zemailer,</p><p>We have curated some new cinematic landscapes for your next project:</p><div style="margin: 20px 0;"><img src="https://images.unsplash.com/photo-1464822759023-fed622ff2c3b?auto=format&fit=crop&w=800&q=80" style="width: 100%; border-radius: 12px; margin-bottom: 15px;" alt="Mountain Landscape"><img src="https://images.unsplash.com/photo-1470770841072-f978cf4d019e?auto=format&fit=crop&w=800&q=80" style="width: 100%; border-radius: 12px;" alt="Lake Landscape"></div><p>Feel free to use them in your designs!</p>',
|
||||
'body_text' => "Hello Zemailer,\n\nWe have curated some new cinematic landscapes for your next project.\n\n[Images are blocked by default in privacy mode]\n\nCheck them out on Unsplash!",
|
||||
'time' => '7:45 AM',
|
||||
'unread' => true,
|
||||
'flagged' => false,
|
||||
'attachments' => [
|
||||
['name' => 'presentation_v1.pdf', 'size' => '4.2 MB'],
|
||||
],
|
||||
],
|
||||
[
|
||||
'id' => 103,
|
||||
'from_name' => 'Figma',
|
||||
'from_email' => 'notifications@figma.com',
|
||||
'subject' => 'Atul Kumar mentioned you in "Mobile App (Draft)"',
|
||||
'preview' => '"@idevakk take a look at the revised QR modal design, let me know if..."',
|
||||
'content' => '<p><strong>Atul Kumar</strong> mentioned you in a comment on <strong>Mobile App (Draft)</strong>:</p><blockquote>"@idevakk take a look at the revised QR modal design, let me know if the proportions look right to you."</blockquote><p>Reply in Figma or view the comment online.</p>',
|
||||
'time' => '8:30 AM',
|
||||
'unread' => false,
|
||||
'flagged' => true,
|
||||
'attachments' => [],
|
||||
],
|
||||
// Generated Inbox items to reach 15
|
||||
],
|
||||
2 => [
|
||||
2 => [ // Sent
|
||||
[
|
||||
'id' => 2,
|
||||
'from_name' => 'Stripe',
|
||||
'from_email' => 'support@stripe.com',
|
||||
'subject' => 'Your weekly payment report',
|
||||
'preview' => 'Your weekly report for the period of Feb 24 - Mar 2 is now available.',
|
||||
'content' => '<p>Hello,</p><p>Your weekly report for the period of Feb 24 - Mar 2 is now available in your dashboard.</p><p>Total Volume: $12,450.00</p><p>View the full report details online.</p>',
|
||||
'body_html' => '<p>Hello,</p><p>Your weekly report for the period of Feb 24 - Mar 2 is now available in your dashboard.</p><p>Total Volume: $12,450.00</p><p>View the full report details online.</p>',
|
||||
'body_text' => "Hello,\n\nYour weekly report for the period of Feb 24 - Mar 2 is now available in your dashboard.\n\nTotal Volume: $12,450.00\n\nView the full report details online.",
|
||||
'time' => 'Yesterday',
|
||||
'unread' => false,
|
||||
'flagged' => false,
|
||||
'attachments' => [
|
||||
['name' => 'report_mar_02.pdf', 'size' => '1.2 MB'],
|
||||
'attachments' => [['name' => 'report_mar_02.pdf', 'size' => '1.2 MB']],
|
||||
],
|
||||
],
|
||||
[
|
||||
'id' => 201,
|
||||
'from_name' => 'Postmark',
|
||||
'from_email' => 'alerts@postmarkapp.com',
|
||||
'subject' => 'Outbound volume spike detected',
|
||||
'preview' => 'We noticed a sudden increase in outbound emails from your "Production" server.',
|
||||
'content' => '<p>Alert: Outbound volume spike.</p><p>Server: Production</p><p>We detected 5,000+ emails sent in the last hour. Please ensure this is expected activity.</p>',
|
||||
'time' => 'Yesterday',
|
||||
'unread' => true,
|
||||
'flagged' => false,
|
||||
'attachments' => [],
|
||||
],
|
||||
[
|
||||
'id' => 202,
|
||||
'from_name' => 'Vercel',
|
||||
'from_email' => 'deployments@vercel.com',
|
||||
'subject' => 'Team "idevakk" deployment successful',
|
||||
'preview' => 'Production deployment for the imail-frontend project has completed.',
|
||||
'content' => '<p>Your deployment is live!</p><p>Project: imail-frontend</p><p>Command: <code>npm run build</code></p><p>View your deployment here: https://imail.app</p>',
|
||||
'time' => 'Mar 2',
|
||||
'unread' => false,
|
||||
'flagged' => false,
|
||||
'attachments' => [],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
return $emails[$this->currentMailboxId] ?? [
|
||||
3 => [ // Notifications
|
||||
[
|
||||
'id' => 3,
|
||||
'from_name' => 'Slack',
|
||||
'from_email' => 'notifications@slack.com',
|
||||
'subject' => 'You have 12 unread messages from your team',
|
||||
'preview' => 'Atul Kumar: "Did you check the new API endpoints? We need them for..."',
|
||||
'content' => '<p>You have new activity in Slack.</p><ul><li><strong>#dev-chat</strong>: 8 new messages</li><li><strong>#announcements</strong>: 4 new messages</li></ul>',
|
||||
'body_html' => '<p>You have new activity in Slack.</p><ul><li><strong>#dev-chat</strong>: 8 new messages</li><li><strong>#announcements</strong>: 4 new messages</li></ul>',
|
||||
'body_text' => "You have new activity in Slack.\n\n#dev-chat: 8 new messages\n#announcements: 4 new messages",
|
||||
'time' => 'Mar 1',
|
||||
'unread' => true,
|
||||
'flagged' => false,
|
||||
'attachments' => [],
|
||||
],
|
||||
[
|
||||
'id' => 301,
|
||||
'from_name' => 'Zoom',
|
||||
'from_email' => 'no-reply@zoom.us',
|
||||
'subject' => 'Meeting Reminder: "Sprint Planning"',
|
||||
'preview' => 'Your Sprint Planning meeting is scheduled to start in 15 minutes.',
|
||||
'content' => '<p>Friendly reminder that your Sprint Planning call starts soon.</p><p>Link: https://zoom.us/j/123456789</p>',
|
||||
'time' => 'Feb 28',
|
||||
'unread' => false,
|
||||
'flagged' => false,
|
||||
'attachments' => [],
|
||||
],
|
||||
];
|
||||
|
||||
// Fill Inbox (MB 1) to 15
|
||||
for ($i = 5; $i <= 18; $i++) {
|
||||
$baseEmails[1][] = [
|
||||
'id' => 1000 + $i,
|
||||
'from_name' => "Partner $i",
|
||||
'from_email' => "partner-$i@example.com",
|
||||
'subject' => "Follow-up proposal #$i",
|
||||
'preview' => "I wanted to check in regarding our previous discussion on project $i...",
|
||||
'body_html' => "<p>Hello,</p><p>This is a follow-up email #$i regarding our partnership.</p>",
|
||||
'body_text' => "Hello,\n\nThis is a follow-up email #$i regarding our partnership.",
|
||||
'time' => 'Mar 1',
|
||||
'unread' => $i % 3 === 0,
|
||||
'flagged' => $i % 5 === 0,
|
||||
'attachments' => [],
|
||||
];
|
||||
}
|
||||
|
||||
// Fill Sent (MB 2) to 15
|
||||
for ($i = 1; $i <= 14; $i++) {
|
||||
$baseEmails[2][] = [
|
||||
'id' => 2000 + $i,
|
||||
'from_name' => 'Me',
|
||||
'from_email' => 'idevakk@imail.com',
|
||||
'subject' => "Re: Project Sync $i",
|
||||
'preview' => "Sounds good, let's proceed with the plan we discussed for sprint $i.",
|
||||
'body_html' => "<p>Hi team,</p><p>Update on project $i: everything is on track.</p>",
|
||||
'body_text' => "Hi team,\n\nUpdate on project $i: everything is on track.",
|
||||
'time' => 'Feb 26',
|
||||
'unread' => false,
|
||||
'flagged' => $i % 4 === 0,
|
||||
'attachments' => [],
|
||||
];
|
||||
}
|
||||
|
||||
// Fill Others (MB 3) to 15
|
||||
for ($i = 1; $i <= 14; $i++) {
|
||||
$baseEmails[3][] = [
|
||||
'id' => 3000 + $i,
|
||||
'from_name' => "System Notification $i",
|
||||
'from_email' => 'noreply@system.com',
|
||||
'subject' => "Security Alert $i: New Login",
|
||||
'preview' => "We detected a new login to your account from a new device on day $i...",
|
||||
'body_html' => "<p>A new login was detected on your account.</p><p>Location: City $i</p>",
|
||||
'body_text' => "A new login was detected on your account.\n\nLocation: City $i",
|
||||
'time' => 'Feb 25',
|
||||
'unread' => $i % 2 === 0,
|
||||
'flagged' => false,
|
||||
'attachments' => [],
|
||||
];
|
||||
}
|
||||
|
||||
$allData = $baseEmails[$this->currentMailboxId] ?? $baseEmails[3];
|
||||
$total = count($allData);
|
||||
$this->totalPages = ceil($total / 10);
|
||||
|
||||
// Ensure page is within bounds
|
||||
if ($this->page > $this->totalPages && $this->totalPages > 0) {
|
||||
$this->page = $this->totalPages;
|
||||
}
|
||||
|
||||
return array_slice($allData, ($this->page - 1) * 10, 10);
|
||||
}
|
||||
|
||||
public function selectEmail($id)
|
||||
{
|
||||
$this->selectedEmailId = $id;
|
||||
$this->viewMode = 'text';
|
||||
$this->allowRemoteContent = false;
|
||||
|
||||
// Simulate cinematic loading
|
||||
usleep(500000); // 500ms
|
||||
}
|
||||
|
||||
public function switchMailbox($id)
|
||||
@@ -172,6 +196,7 @@ class Mailbox extends Component
|
||||
$this->currentMailboxId = $id;
|
||||
$this->selectedEmailId = null;
|
||||
$this->search = '';
|
||||
$this->page = 1;
|
||||
}
|
||||
|
||||
public function createMailbox()
|
||||
@@ -226,6 +251,7 @@ class Mailbox extends Component
|
||||
{
|
||||
if ($this->page < $this->totalPages) {
|
||||
$this->page++;
|
||||
$this->selectedEmailId = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -233,6 +259,7 @@ class Mailbox extends Component
|
||||
{
|
||||
if ($this->page > 1) {
|
||||
$this->page--;
|
||||
$this->selectedEmailId = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -243,6 +270,29 @@ class Mailbox extends Component
|
||||
$this->dispatch('qrCodeGenerated', address: $address);
|
||||
}
|
||||
|
||||
public function getProcessedContent($email)
|
||||
{
|
||||
$content = $email['body_html'];
|
||||
$isText = $this->viewMode === 'text';
|
||||
|
||||
// Fallback to HTML if text is selected but body_text is empty
|
||||
if ($isText && ! empty($email['body_text'])) {
|
||||
return trim(e($email['body_text']));
|
||||
}
|
||||
|
||||
if ($isText) {
|
||||
// If fallback occurred, we sanitize the HTML to text
|
||||
return trim(strip_tags($content));
|
||||
}
|
||||
|
||||
if (! $this->allowRemoteContent) {
|
||||
// Block remote assets by replacing src with data-src for img tags
|
||||
return preg_replace('/<img\s[^>]*?\bsrc\s*=\s*([\'"])(.*?)\1/i', '<img $2 data-blocked-src=$1$2$1 src="data:image/svg+xml,%3Csvg xmlns=\'http://www.w3.org/2000/svg\' viewBox=\'0 0 1 1\'%3E%3C/svg%3E" class="blocked-remote-asset shadow-sm border border-white/5 opacity-50"', $content);
|
||||
}
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
$currentMailbox = collect($this->activeMailboxes)->firstWhere('id', $this->currentMailboxId);
|
||||
|
||||
162
laravel_webhook_handover.md
Normal file
162
laravel_webhook_handover.md
Normal file
@@ -0,0 +1,162 @@
|
||||
# MailOps Webhook Handover Document
|
||||
|
||||
This document provides the exact specifications needed to implement the receiving end of the MailOps email synchronization system within the Laravel application.
|
||||
|
||||
## 1. Webhook Endpoint Specification
|
||||
|
||||
The MailOps worker will push new emails to this exact endpoint on your Laravel server:
|
||||
|
||||
* **URL:** `POST https://your-laravel-app.com/api/webhooks/incoming_email`
|
||||
* **Headers:**
|
||||
* `Content-Type: application/json`
|
||||
* `Authorization: Bearer <CONFIGURED_WEBHOOK_SECRET>` (You must configure this secret in both MailOps and Laravel).
|
||||
|
||||
### A. Expected JSON Payload (With Attachments)
|
||||
|
||||
```json
|
||||
{
|
||||
"hash": "a1b2c3d4e5f6g7h8i9j0...",
|
||||
"metadata": {
|
||||
"hash": "a1b2c3d4e5f6g7h8i9j0...",
|
||||
"recipientEmail": "user@example.com",
|
||||
"recipientName": "John Doe",
|
||||
"senderEmail": "alert@service.com",
|
||||
"senderName": "Service Alerts",
|
||||
"domain": "example.com",
|
||||
"subject": "Important Notification",
|
||||
"received_at": "2026-02-26T17:35:00Z",
|
||||
"attachments": [
|
||||
{
|
||||
"filename": "invoice.pdf",
|
||||
"mimeType": "application/pdf",
|
||||
"size": 102400,
|
||||
"s3_path": "mail-attachments/2026/02/26/hash_invoice.pdf"
|
||||
}
|
||||
],
|
||||
"attachmentSize": 102400
|
||||
},
|
||||
"bodyText": "Plain text content...",
|
||||
"bodyHtml": "<html>HTML content...</html>"
|
||||
}
|
||||
```
|
||||
*(Note: `received_at` is in ISO 8601 format ending with `Z` to explicitly denote UTC. `bodyHtml` and `bodyText` are completely separated from the metadata to optimize database payload sizes).*
|
||||
|
||||
### B. Expected JSON Payload (NO Attachments)
|
||||
When an email has no attachments, the `attachments` array will be empty and `attachmentSize` will be zero. Also, depending on the email client, `bodyHtml` or `bodyText` might be `null`.
|
||||
|
||||
```json
|
||||
{
|
||||
"hash": "b2c3d4e5f6g7h8i9j0a1...",
|
||||
"metadata": {
|
||||
"hash": "b2c3d4e5f6g7h8i9j0a1...",
|
||||
"recipientEmail": "user@example.com",
|
||||
"recipientName": "",
|
||||
"senderEmail": "friend@service.com",
|
||||
"senderName": "Friend",
|
||||
"domain": "example.com",
|
||||
"subject": "Quick Question",
|
||||
"received_at": "2026-02-26T17:38:12Z",
|
||||
"attachments": [],
|
||||
"attachmentSize": 0
|
||||
},
|
||||
"bodyText": "Hey, are we still fast approaching the deadline?",
|
||||
"bodyHtml": null
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Laravel Implementation Checklist
|
||||
|
||||
When you switch to the Laravel project, you need to build the following:
|
||||
|
||||
### Step 1: Route & Middleware
|
||||
Define the API route and protect it with a simple Bearer token check.
|
||||
```php
|
||||
// routes/api.php
|
||||
Route::post('/webhooks/incoming_email', [EmailWebhookController::class, 'handle'])
|
||||
->middleware('verify.webhook.secret');
|
||||
```
|
||||
|
||||
### Step 2: The Controller
|
||||
The controller persists the metadata to MariaDB and the heavy body to MongoDB. **Crucially**, it also checks if the MongoDB TTL index exists, and if not, automatically creates it using the value defined in your Laravel `.env` file (e.g., `EMAIL_BODY_TTL_SECONDS=259200`).
|
||||
|
||||
```php
|
||||
// app/Http/Controllers/EmailWebhookController.php
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
public function handle(Request $request)
|
||||
{
|
||||
$payload = $request->all();
|
||||
$meta = $payload['metadata'];
|
||||
$hash = $payload['hash'];
|
||||
|
||||
// 1. Auto-Setup MongoDB TTL Index (Executes only once via Cache)
|
||||
$this->ensureMongoTtlIndexExists();
|
||||
|
||||
// 2. MariaDB: Save Metadata
|
||||
Email::updateOrCreate(
|
||||
['unique_id_hash' => $hash],
|
||||
[
|
||||
'recipient_email' => $meta['recipientEmail'],
|
||||
'sender_email' => $meta['senderEmail'],
|
||||
'subject' => $meta['subject'] ?? '',
|
||||
'is_read' => false,
|
||||
// Parse the ISO 8601 UTC timestamp format explicitly for SQL
|
||||
'received_at' => Carbon::parse($meta['received_at'])->setTimezone('UTC')->toDateTimeString(),
|
||||
// Store attachments JSON. If empty, ensure it's saved as an empty array '[]'
|
||||
'attachments' => !empty($meta['attachments']) ? json_encode($meta['attachments']) : '[]',
|
||||
'attachment_size' => $meta['attachmentSize'] ?? 0
|
||||
]
|
||||
);
|
||||
|
||||
// 3. MongoDB: Save the heavy body with TTL
|
||||
// Assuming you have the jenssegers/mongodb package installed
|
||||
RecentEmailBody::updateOrCreate(
|
||||
['unique_id_hash' => $hash],
|
||||
[
|
||||
// Handle cases where the sender only sends Text or only HTML
|
||||
'body_text' => $payload['bodyText'] ?? '',
|
||||
'body_html' => $payload['bodyHtml'] ?? '',
|
||||
'created_at' => new \MongoDB\BSON\UTCDateTime(now()->timestamp * 1000), // BSON required for TTL
|
||||
]
|
||||
);
|
||||
|
||||
return response()->json(['status' => 'success'], 200);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures the TTL index is created on the MongoDB collection.
|
||||
* Uses Laravel Cache to avoid checking the database on every single webhook.
|
||||
*/
|
||||
private function ensureMongoTtlIndexExists()
|
||||
{
|
||||
Cache::rememberForever('mongo_ttl_index_created', function () {
|
||||
// Fetch TTL from Laravel .env (Default: 72 hours / 259200 seconds)
|
||||
$ttlSeconds = (int) env('EMAIL_BODY_TTL_SECONDS', 259200);
|
||||
|
||||
$collection = DB::connection('mongodb')->getCollection('recent_email_bodies');
|
||||
|
||||
// Background creation prevents locking the database during webhook execution
|
||||
$collection->createIndex(
|
||||
['created_at' => 1],
|
||||
[
|
||||
'expireAfterSeconds' => $ttlSeconds,
|
||||
'background' => true,
|
||||
'name' => 'ttl_created_at_index' // Named index prevents duplicate recreation errors
|
||||
]
|
||||
);
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Resiliency Notes
|
||||
|
||||
* **Idempotency:** The MailOps worker might retry a webhook if a network timeout occurs even after Laravel successfully saved it. Your Laravel code MUST use `updateOrCreate` or `INSERT IGNORE` (like the example above) so it doesn't create duplicate emails if the same payload hash is received twice.
|
||||
* **Timeouts:** The MailOps worker expects a response within 5 to 10 seconds. Do not perform long-running synchronous tasks (like connecting to external APIs or sending heavy push notifications) inside the webhook controller. Dispatch those to a Laravel Queue instead.
|
||||
@@ -46,9 +46,9 @@
|
||||
'xl:w-20': !sidebarOpen && window.innerWidth >= 1280
|
||||
}">
|
||||
<div class="h-16 flex items-center justify-between px-6 border-b border-white/5">
|
||||
<div class="flex items-center gap-3">
|
||||
<a href="/" class="flex items-center gap-3 hover:opacity-80 transition-opacity">
|
||||
<x-bento.logo size="sm" x-data="{ showText: sidebarOpen }" />
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<!-- Mobile Close Button -->
|
||||
<button @click="sidebarOpen = false" class="xl:hidden p-1.5 rounded-lg hover:bg-white/5 text-zinc-500 hover:text-white transition-all">
|
||||
@@ -213,13 +213,13 @@
|
||||
</div>
|
||||
|
||||
<!-- List Content -->
|
||||
<div class="flex-1 overflow-y-auto divide-y divide-white/5 scrollbar-hide">
|
||||
<div class="flex-1 overflow-y-auto divide-y divide-white/5 scrollbar-hide" x-ref="listContainer">
|
||||
@foreach($emails as $email)
|
||||
<div @click="$wire.selectEmail({{ $email['id'] }}); mobileView = 'detail'"
|
||||
<div wire:key="email-{{ $email['id'] }}"
|
||||
@click="$wire.selectEmail({{ $email['id'] }}); mobileView = 'detail'"
|
||||
class="p-5 cursor-pointer transition-all relative group"
|
||||
:class="selectedId === {{ $email['id'] }} ? 'bg-pink-500/5' : 'hover:bg-white/[0.02]'">
|
||||
<!-- Active Indicator -->
|
||||
<div class="absolute left-0 top-0 bottom-0 w-1 bg-pink-500 transition-transform duration-300"
|
||||
<div class="absolute left-0 top-0 bottom-0 w-1 bg-pink-500 scale-y-0 transition-transform duration-300"
|
||||
:class="selectedId === {{ $email['id'] }} ? 'scale-y-100' : 'scale-y-0'"></div>
|
||||
|
||||
<div class="flex items-start justify-between gap-4 mb-2">
|
||||
@@ -257,6 +257,7 @@
|
||||
<!-- Sticky Pagination -->
|
||||
<div class="h-14 flex items-center justify-between px-4 border-t border-white/5 bg-zinc-950/40 backdrop-blur-xl shrink-0">
|
||||
<button wire:click="previousPage"
|
||||
@click="$refs.listContainer.scrollTop = 0"
|
||||
class="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-white/5 border border-white/5 text-[10px] font-bold text-zinc-400 hover:text-white hover:bg-white/10 transition-all disabled:opacity-30 disabled:pointer-events-none uppercase tracking-widest"
|
||||
{{ $page <= 1 ? 'disabled' : '' }}>
|
||||
<svg class="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" /></svg>
|
||||
@@ -270,6 +271,7 @@
|
||||
</div>
|
||||
|
||||
<button wire:click="nextPage"
|
||||
@click="$refs.listContainer.scrollTop = 0"
|
||||
class="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-white/5 border border-white/5 text-[10px] font-bold text-zinc-400 hover:text-white hover:bg-white/10 transition-all disabled:opacity-30 disabled:pointer-events-none uppercase tracking-widest"
|
||||
{{ $page >= $totalPages ? 'disabled' : '' }}>
|
||||
Next
|
||||
@@ -328,9 +330,33 @@
|
||||
</div>
|
||||
|
||||
<!-- Email Detail Column -->
|
||||
<main class="flex-1 flex flex-col min-w-0 bg-zinc-950/50 backdrop-blur-md relative z-10 transition-all duration-300"
|
||||
<main class="flex-1 flex flex-col min-w-0 bg-zinc-950/50 backdrop-blur-md relative z-10 transition-all duration-300 h-full overflow-hidden"
|
||||
:class="{'hidden lg:flex': mobileView === 'list' || !selectedId}">
|
||||
|
||||
<!-- Cinematic Loading Overlay -->
|
||||
<div wire:loading.flex wire:target="selectEmail" class="absolute inset-0 z-[100] bg-zinc-950/60 backdrop-blur-xl items-center justify-center">
|
||||
<div class="flex flex-col items-center justify-center">
|
||||
<div class="w-20 h-20 relative mb-8">
|
||||
<div class="absolute inset-0 border-4 border-pink-500/10 rounded-full"></div>
|
||||
<div class="absolute inset-0 border-4 border-pink-500 border-t-transparent animate-spin rounded-full shadow-[0_0_20px_rgba(236,72,153,0.2)]"></div>
|
||||
<div class="absolute inset-4 bg-pink-500/20 rounded-full blur-2xl animate-pulse"></div>
|
||||
<div class="absolute inset-0 flex items-center justify-center">
|
||||
<svg class="w-8 h-8 text-pink-500 animate-pulse" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col items-center">
|
||||
<span class="text-[10px] font-black text-white uppercase tracking-[0.4em] mb-2 animate-pulse">Establishing Secure Channel</span>
|
||||
<div class="flex gap-1">
|
||||
<div class="w-1 h-1 rounded-full bg-pink-500 animate-bounce [animation-delay:-0.3s]"></div>
|
||||
<div class="w-1 h-1 rounded-full bg-pink-500 animate-bounce [animation-delay:-0.15s]"></div>
|
||||
<div class="w-1 h-1 rounded-full bg-pink-500 animate-bounce"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@php $currentEmail = $selectedEmailId ? collect($emails)->firstWhere('id', $selectedEmailId) : null; @endphp
|
||||
@if($currentEmail)
|
||||
|
||||
@@ -362,10 +388,25 @@
|
||||
<svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0" /></svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Privacy Toggles -->
|
||||
<div class="ml-auto flex items-center p-1 bg-white/5 border border-white/5 rounded-xl">
|
||||
<button wire:click="$set('viewMode', 'text')"
|
||||
class="px-3 py-1.5 rounded-lg text-[10px] font-bold uppercase tracking-widest transition-all"
|
||||
:class="$wire.viewMode === 'text' ? 'bg-zinc-800 text-white shadow-lg' : 'text-zinc-500 hover:text-zinc-300'">
|
||||
Text
|
||||
</button>
|
||||
<button wire:click="$set('viewMode', 'html')"
|
||||
class="px-3 py-1.5 rounded-lg text-[10px] font-bold uppercase tracking-widest transition-all"
|
||||
:class="$wire.viewMode === 'html' ? 'bg-zinc-800 text-white shadow-lg' : 'text-zinc-500 hover:text-zinc-300'">
|
||||
HTML
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Detail Content -->
|
||||
<div class="flex-1 overflow-y-auto p-8 lg:p-12 scrollbar-hide">
|
||||
<div class="flex-1 relative overflow-hidden">
|
||||
<div class="h-full overflow-y-auto p-8 lg:p-12 scrollbar-hide" wire:loading.remove wire:target="selectEmail">
|
||||
<div class="max-w-3xl">
|
||||
<div class="flex flex-col md:flex-row md:items-center justify-between gap-6 mb-12">
|
||||
<div class="flex items-center gap-4">
|
||||
@@ -386,9 +427,57 @@
|
||||
{{ $currentEmail['subject'] }}
|
||||
</h1>
|
||||
|
||||
<div class="prose prose-invert max-w-none text-zinc-400 leading-relaxed space-y-4 font-medium mb-12">
|
||||
{!! $currentEmail['content'] !!}
|
||||
@if($viewMode === 'html')
|
||||
<div class="mb-8 p-4 rounded-2xl border transition-all duration-500 @if(!$allowRemoteContent) bg-amber-500/5 border-amber-500/20 @else bg-emerald-500/5 border-emerald-500/20 @endif flex flex-col sm:flex-row items-center justify-between gap-4 group">
|
||||
<div class="flex items-center gap-3">
|
||||
@if(!$allowRemoteContent)
|
||||
<div class="w-10 h-10 rounded-xl bg-amber-500/10 text-amber-500 flex items-center justify-center shrink-0">
|
||||
<svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" /></svg>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-xs font-bold text-amber-500 uppercase tracking-widest leading-none mb-1">Remote assets blocked</div>
|
||||
<p class="text-[10px] text-zinc-500 font-medium">For your privacy, Zemail has disabled automatic loading of remote images.</p>
|
||||
</div>
|
||||
@else
|
||||
<div class="w-10 h-10 rounded-xl bg-emerald-500/10 text-emerald-500 flex items-center justify-center shrink-0">
|
||||
<svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" /></svg>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-xs font-bold text-emerald-500 uppercase tracking-widest leading-none mb-1">Remote content enabled</div>
|
||||
<p class="text-[10px] text-zinc-500 font-medium">Images and remote resources are currently active for this email.</p>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@if(!$allowRemoteContent)
|
||||
<button wire:click="$set('allowRemoteContent', true)"
|
||||
class="px-4 py-2 rounded-xl bg-amber-500 text-zinc-950 font-black text-[10px] uppercase tracking-wider hover:bg-amber-400 transition-all flex items-center gap-2">
|
||||
<svg class="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" /></svg>
|
||||
Load Content
|
||||
</button>
|
||||
@else
|
||||
<button wire:click="$set('allowRemoteContent', false)"
|
||||
class="px-4 py-2 rounded-xl bg-zinc-800 text-zinc-400 font-bold text-[10px] uppercase tracking-wider hover:text-white transition-all flex items-center gap-2 border border-white/5">
|
||||
<svg class="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636" /></svg>
|
||||
Re-Block
|
||||
</button>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if($viewMode === 'text' && empty($currentEmail['body_text']))
|
||||
<div class="mb-8 p-4 rounded-2xl bg-zinc-900 border border-white/5 flex items-center gap-4">
|
||||
<div class="w-10 h-10 rounded-xl bg-white/5 text-zinc-400 flex items-center justify-center shrink-0">
|
||||
<svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-[10px] font-bold text-zinc-300 uppercase tracking-widest leading-none mb-1">Plain text version missing</div>
|
||||
<p class="text-[9px] text-zinc-500 font-medium">Viewing sanitized HTML version instead.</p>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="prose prose-invert max-w-none text-zinc-400 leading-relaxed font-medium mb-12 {{ $viewMode === 'text' ? 'whitespace-pre-wrap font-mono text-[13px] shadow-[inset_0_20px_50px_rgba(0,0,0,0.1)] p-8 bg-zinc-900/30 rounded-3xl border border-white/5 tracking-tight' : 'space-y-4' }}">{!! $this->getProcessedContent($currentEmail) !!}</div>
|
||||
|
||||
@if(count($currentEmail['attachments']) > 0)
|
||||
<div class="mt-12 pt-8 border-t border-white/5">
|
||||
@@ -431,9 +520,10 @@
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<!-- Empty State -->
|
||||
<div class="flex-1 flex flex-col items-center justify-center p-12 text-center">
|
||||
<div class="flex-1 flex flex-col items-center justify-center p-12 text-center" wire:loading.remove wire:target="selectEmail">
|
||||
<div class="w-24 h-24 rounded-3xl bg-zinc-900 border border-white/5 flex items-center justify-center text-zinc-700 mb-8 relative">
|
||||
<div class="absolute inset-0 bg-pink-500/5 rounded-3xl blur-2xl"></div>
|
||||
<svg class="w-12 h-12 relative z-10" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" /></svg>
|
||||
|
||||
Reference in New Issue
Block a user