浏览代码

Merge pull request #5325 from pixelfed/staging

Add Admin Contact Responses
daniel 9 月之前
父节点
当前提交
c46023456f

+ 2 - 1
CHANGELOG.md

@@ -5,6 +5,7 @@
 ### Added
 ### Added
 - Implement Admin Domain Blocks API (Mastodon API Compatible) [ThisIsMissEm](https://github.com/ThisIsMissEm) ([#5021](https://github.com/pixelfed/pixelfed/pull/5021))
 - Implement Admin Domain Blocks API (Mastodon API Compatible) [ThisIsMissEm](https://github.com/ThisIsMissEm) ([#5021](https://github.com/pixelfed/pixelfed/pull/5021))
 - Authorize Interaction support (for handling remote interactions) ([4ca7c6c3](https://github.com/pixelfed/pixelfed/commit/4ca7c6c3))
 - Authorize Interaction support (for handling remote interactions) ([4ca7c6c3](https://github.com/pixelfed/pixelfed/commit/4ca7c6c3))
+- Contact Form Admin Responses ([52cc6090](https://github.com/pixelfed/pixelfed/commit/52cc6090))
 
 
 ### Federation
 ### Federation
 - Add ActiveSharedInboxService, for efficient sharedInbox caching ([1a6a3397](https://github.com/pixelfed/pixelfed/commit/1a6a3397))
 - Add ActiveSharedInboxService, for efficient sharedInbox caching ([1a6a3397](https://github.com/pixelfed/pixelfed/commit/1a6a3397))
@@ -31,7 +32,7 @@
 - Update layout, add og:logo ([4cc576e1](https://github.com/pixelfed/pixelfed/commit/4cc576e1))
 - Update layout, add og:logo ([4cc576e1](https://github.com/pixelfed/pixelfed/commit/4cc576e1))
 - Update ReblogService, fix cache sync issues ([3de8ceca](https://github.com/pixelfed/pixelfed/commit/3de8ceca))
 - Update ReblogService, fix cache sync issues ([3de8ceca](https://github.com/pixelfed/pixelfed/commit/3de8ceca))
 - Update config, allow Beagle discover service to be disabled ([de4ce3c8](https://github.com/pixelfed/pixelfed/commit/de4ce3c8))
 - Update config, allow Beagle discover service to be disabled ([de4ce3c8](https://github.com/pixelfed/pixelfed/commit/de4ce3c8))
--  ([](https://github.com/pixelfed/pixelfed/commit/))
+- Update ApiV1Dot1Controller, allow upto 5 similar push tokens ([7820b506](https://github.com/pixelfed/pixelfed/commit/7820b506))
 -  ([](https://github.com/pixelfed/pixelfed/commit/))
 -  ([](https://github.com/pixelfed/pixelfed/commit/))
 
 
 ## [v0.12.3 (2024-07-01)](https://github.com/pixelfed/pixelfed/compare/v0.12.2...v0.12.3)
 ## [v0.12.3 (2024-07-01)](https://github.com/pixelfed/pixelfed/compare/v0.12.2...v0.12.3)

+ 17 - 2
app/Contact.php

@@ -3,16 +3,31 @@
 namespace App;
 namespace App;
 
 
 use Illuminate\Database\Eloquent\Model;
 use Illuminate\Database\Eloquent\Model;
+use Illuminate\Support\Str;
 
 
 class Contact extends Model
 class Contact extends Model
 {
 {
+    protected $casts = [
+        'responded_at' => 'datetime',
+    ];
+
     public function user()
     public function user()
     {
     {
-    	return $this->belongsTo(User::class);
+        return $this->belongsTo(User::class);
     }
     }
 
 
     public function adminUrl()
     public function adminUrl()
     {
     {
-    	return url('/i/admin/messages/show/' . $this->id);
+        return url('/i/admin/messages/show/'.$this->id);
+    }
+
+    public function userResponseUrl()
+    {
+        return url('/i/contact-admin-response/'.$this->id);
+    }
+
+    public function getMessageId()
+    {
+        return $this->id.'-'.(string) Str::uuid().'@'.strtolower(config('pixelfed.domain.app', 'example.org'));
     }
     }
 }
 }

+ 82 - 3
app/Http/Controllers/AdminController.php

@@ -13,6 +13,7 @@ use App\Http\Controllers\Admin\AdminReportController;
 use App\Http\Controllers\Admin\AdminSettingsController;
 use App\Http\Controllers\Admin\AdminSettingsController;
 use App\Http\Controllers\Admin\AdminUserController;
 use App\Http\Controllers\Admin\AdminUserController;
 use App\Instance;
 use App\Instance;
+use App\Mail\AdminMessageResponse;
 use App\Models\CustomEmoji;
 use App\Models\CustomEmoji;
 use App\Newsroom;
 use App\Newsroom;
 use App\OauthClient;
 use App\OauthClient;
@@ -29,6 +30,7 @@ use Cache;
 use DB;
 use DB;
 use Illuminate\Http\Request;
 use Illuminate\Http\Request;
 use Illuminate\Validation\Rule;
 use Illuminate\Validation\Rule;
+use Mail;
 use Storage;
 use Storage;
 
 
 class AdminController extends Controller
 class AdminController extends Controller
@@ -221,18 +223,86 @@ class AdminController extends Controller
 
 
     public function messagesHome(Request $request)
     public function messagesHome(Request $request)
     {
     {
-        $messages = Contact::orderByDesc('id')->paginate(10);
+        $this->validate($request, [
+            'sort' => 'sometimes|string|in:all,open,closed',
+        ]);
+        $sort = $request->input('sort', 'open');
 
 
-        return view('admin.messages.home', compact('messages'));
+        $messages = Contact::when($sort, function ($query, $sort) {
+            if ($sort === 'open') {
+                $query->whereNull('read_at');
+            }
+            if ($sort === 'closed') {
+                $query->whereNotNull('read_at');
+            }
+        })
+            ->orderByDesc('id')
+            ->paginate(10)
+            ->withQueryString();
+
+        return view('admin.messages.home', compact('messages', 'sort'));
     }
     }
 
 
     public function messagesShow(Request $request, $id)
     public function messagesShow(Request $request, $id)
     {
     {
         $message = Contact::findOrFail($id);
         $message = Contact::findOrFail($id);
+        $user = User::whereNull('status')->find($message->user_id);
+        if(!$user) {
+            $message->read_at = now();
+            $message->save();
+            return redirect('/i/admin/messages/home')->with('status', 'Redirected from message sent from a deleted account');
+        }
 
 
         return view('admin.messages.show', compact('message'));
         return view('admin.messages.show', compact('message'));
     }
     }
 
 
+    public function messagesReply(Request $request, $id)
+    {
+        $this->validate($request, [
+            'message' => 'required|string|min:1|max:500',
+        ]);
+
+        if(config('mail.default') === 'log') {
+            return redirect('/i/admin/messages/home')->with('error', 'Mail driver not configured, please setup before you can sent email.');
+        }
+
+        $message = Contact::whereNull('responded_at')->findOrFail($id);
+        $user = User::whereNull('status')->find($message->user_id);
+        if(!$user) {
+            $message->read_at = now();
+            $message->save();
+            return redirect('/i/admin/messages/home')->with('status', 'Redirected from message sent from a deleted account');
+        }
+        $message->response = $request->input('message');
+        $message->read_at = now();
+        $message->responded_at = now();
+        $message->save();
+
+        Mail::to($message->user->email)->send(new AdminMessageResponse($message));
+
+        return redirect('/i/admin/messages/home')->with('status', 'Sent response to '.$message->user->username);
+    }
+
+    public function messagesReplyPreview(Request $request, $id)
+    {
+        $this->validate($request, [
+            'message' => 'required|string|min:1|max:500',
+        ]);
+
+        if(config('mail.default') === 'log') {
+            return redirect('/i/admin/messages/home')->with('error', 'Mail driver not configured, please setup before you can sent email.');
+        }
+
+        $message = Contact::whereNull('read_at')->findOrFail($id);
+        $user = User::whereNull('status')->find($message->user_id);
+        if(!$user) {
+            $message->read_at = now();
+            $message->save();
+            return redirect('/i/admin/messages/home')->with('error', 'Redirected from message sent from a deleted account');
+        }
+        return new AdminMessageResponse($message);
+    }
+
     public function messagesMarkRead(Request $request)
     public function messagesMarkRead(Request $request)
     {
     {
         $this->validate($request, [
         $this->validate($request, [
@@ -240,12 +310,21 @@ class AdminController extends Controller
         ]);
         ]);
         $id = $request->input('id');
         $id = $request->input('id');
         $message = Contact::findOrFail($id);
         $message = Contact::findOrFail($id);
+
+        $user = User::whereNull('status')->find($message->user_id);
+        if(!$user) {
+            $message->read_at = now();
+            $message->save();
+            return redirect('/i/admin/messages/home')->with('error', 'Redirected from message sent from a deleted account');
+        }
         if ($message->read_at) {
         if ($message->read_at) {
             return;
             return;
         }
         }
         $message->read_at = now();
         $message->read_at = now();
         $message->save();
         $message->save();
+        $request->session()->flash('status', 'Marked response from '.$message->user->username.' as read!');
 
 
+        return ['status' => 200];
     }
     }
 
 
     public function newsroomHome(Request $request)
     public function newsroomHome(Request $request)
@@ -355,7 +434,7 @@ class AdminController extends Controller
         if (Newsroom::whereSlug($slug)->exists()) {
         if (Newsroom::whereSlug($slug)->exists()) {
             $slug = $slug.'-'.str_random(4);
             $slug = $slug.'-'.str_random(4);
         }
         }
-        $news = new Newsroom();
+        $news = new Newsroom;
         $fields = [
         $fields = [
             'title' => 'string',
             'title' => 'string',
             'summary' => 'string',
             'summary' => 'string',

+ 11 - 0
app/Http/Controllers/ContactController.php

@@ -50,4 +50,15 @@ class ContactController extends Controller
 
 
 		return redirect()->back()->with('status', 'Success - Your message has been sent to admins.');
 		return redirect()->back()->with('status', 'Success - Your message has been sent to admins.');
 	}
 	}
+
+    public function showAdminResponse(Request $request, $id)
+    {
+        abort_if(!$request->user(), 404);
+        $uid = $request->user()->id;
+        $contact = Contact::whereUserId($uid)
+            ->whereNotNull('response')
+            ->whereNotNull('responded_at')
+            ->findOrFail($id);
+        return view('site.contact.admin-response', compact('contact'));
+    }
 }
 }

+ 71 - 0
app/Mail/AdminMessageResponse.php

@@ -0,0 +1,71 @@
+<?php
+
+namespace App\Mail;
+
+use App\Contact;
+use Illuminate\Bus\Queueable;
+use Illuminate\Mail\Mailable;
+use Illuminate\Mail\Mailables\Content;
+use Illuminate\Mail\Mailables\Envelope;
+use Illuminate\Mail\Mailables\Headers;
+use Illuminate\Queue\SerializesModels;
+
+class AdminMessageResponse extends Mailable
+{
+    use Queueable, SerializesModels;
+
+    /**
+     * Create a new message instance.
+     */
+    public function __construct(
+        public Contact $contact,
+    ) {}
+
+    /**
+     * Get the message headers.
+     */
+    public function headers(): Headers
+    {
+        $mid = $this->contact->getMessageId();
+
+        return new Headers(
+            messageId: $mid,
+            text: [
+                'X-Entity-Ref-ID' => $mid,
+            ],
+        );
+    }
+
+    /**
+     * Get the message envelope.
+     */
+    public function envelope(): Envelope
+    {
+        return new Envelope(
+            subject: ucfirst(strtolower(config('pixelfed.domain.app'))).' Contact Form Response [Ticket #'.$this->contact->id.']',
+        );
+    }
+
+    /**
+     * Get the message content definition.
+     */
+    public function content(): Content
+    {
+        return new Content(
+            markdown: 'emails.contact.admin-response',
+            with: [
+                'url' => $this->contact->userResponseUrl(),
+            ],
+        );
+    }
+
+    /**
+     * Get the attachments for the message.
+     *
+     * @return array<int, \Illuminate\Mail\Mailables\Attachment>
+     */
+    public function attachments(): array
+    {
+        return [];
+    }
+}

+ 1 - 0
composer.json

@@ -37,6 +37,7 @@
 		"pragmarx/google2fa": "^8.0",
 		"pragmarx/google2fa": "^8.0",
 		"predis/predis": "^2.0",
 		"predis/predis": "^2.0",
 		"pusher/pusher-php-server": "^7.2",
 		"pusher/pusher-php-server": "^7.2",
+		"resend/resend-php": "^0.13.0",
 		"spatie/laravel-backup": "^8.0.0",
 		"spatie/laravel-backup": "^8.0.0",
 		"spatie/laravel-image-optimizer": "^1.8.0",
 		"spatie/laravel-image-optimizer": "^1.8.0",
 		"stevebauman/purify": "^6.2.0",
 		"stevebauman/purify": "^6.2.0",

+ 58 - 1
composer.lock

@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
         "This file is @generated automatically"
     ],
     ],
-    "content-hash": "fecc0efcc40880a422690feefedef584",
+    "content-hash": "0035325cb0240e92fc378e49f76447bd",
     "packages": [
     "packages": [
         {
         {
             "name": "aws/aws-crt-php",
             "name": "aws/aws-crt-php",
@@ -6378,6 +6378,63 @@
             ],
             ],
             "time": "2024-04-27T21:32:50+00:00"
             "time": "2024-04-27T21:32:50+00:00"
         },
         },
+        {
+            "name": "resend/resend-php",
+            "version": "v0.13.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/resend/resend-php.git",
+                "reference": "c74926e34472fe3e3e21f150f3e3ce56fcbf8298"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/resend/resend-php/zipball/c74926e34472fe3e3e21f150f3e3ce56fcbf8298",
+                "reference": "c74926e34472fe3e3e21f150f3e3ce56fcbf8298",
+                "shasum": ""
+            },
+            "require": {
+                "guzzlehttp/guzzle": "^7.5",
+                "php": "^8.1.0"
+            },
+            "require-dev": {
+                "friendsofphp/php-cs-fixer": "^3.13",
+                "mockery/mockery": "^1.6",
+                "pestphp/pest": "^2.0"
+            },
+            "type": "library",
+            "autoload": {
+                "files": [
+                    "src/Resend.php"
+                ],
+                "psr-4": {
+                    "Resend\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Resend and contributors",
+                    "homepage": "https://github.com/resend/resend-php/contributors"
+                }
+            ],
+            "description": "Resend PHP library.",
+            "homepage": "https://resend.com/",
+            "keywords": [
+                "api",
+                "client",
+                "php",
+                "resend",
+                "sdk"
+            ],
+            "support": {
+                "issues": "https://github.com/resend/resend-php/issues",
+                "source": "https://github.com/resend/resend-php/tree/v0.13.0"
+            },
+            "time": "2024-08-15T03:27:29+00:00"
+        },
         {
         {
             "name": "spatie/db-dumper",
             "name": "spatie/db-dumper",
             "version": "3.7.0",
             "version": "3.7.0",

+ 13 - 9
config/mail.php

@@ -37,8 +37,8 @@ return [
         'smtp' => [
         'smtp' => [
             'transport' => 'smtp',
             'transport' => 'smtp',
             'url' => env('MAIL_URL'),
             'url' => env('MAIL_URL'),
-            'host' => env('MAIL_HOST', 'smtp.mailgun.org'),
-            'port' => env('MAIL_PORT', 587),
+            'host' => env('MAIL_HOST', '127.0.0.1'),
+            'port' => env('MAIL_PORT', 2525),
             'encryption' => env('MAIL_ENCRYPTION', 'tls'),
             'encryption' => env('MAIL_ENCRYPTION', 'tls'),
             'username' => env('MAIL_USERNAME'),
             'username' => env('MAIL_USERNAME'),
             'password' => env('MAIL_PASSWORD'),
             'password' => env('MAIL_PASSWORD'),
@@ -53,16 +53,21 @@ return [
 
 
         'mailgun' => [
         'mailgun' => [
             'transport' => 'mailgun',
             'transport' => 'mailgun',
-            // 'client' => [
-            //     'timeout' => 5,
-            // ],
+            'client' => [
+                'timeout' => 5,
+            ],
         ],
         ],
 
 
         'postmark' => [
         'postmark' => [
             'transport' => 'postmark',
             'transport' => 'postmark',
-            // 'client' => [
-            //     'timeout' => 5,
-            // ],
+            'message_stream_id' => env('POSTMARK_MESSAGE_STREAM_ID'),
+            'client' => [
+                'timeout' => 5,
+            ],
+        ],
+
+        'resend' => [
+            'transport' => 'resend',
         ],
         ],
 
 
         'sendmail' => [
         'sendmail' => [
@@ -82,7 +87,6 @@ return [
         'failover' => [
         'failover' => [
             'transport' => 'failover',
             'transport' => 'failover',
             'mailers' => [
             'mailers' => [
-                'smtp',
                 'log',
                 'log',
             ],
             ],
         ],
         ],

+ 7 - 3
config/services.php

@@ -20,7 +20,7 @@ return [
     ],
     ],
 
 
     'ses' => [
     'ses' => [
-        'key'    => env('SES_KEY'),
+        'key' => env('SES_KEY'),
         'secret' => env('SES_SECRET'),
         'secret' => env('SES_SECRET'),
         'region' => env('SES_REGION', 'us-east-1'),
         'region' => env('SES_REGION', 'us-east-1'),
     ],
     ],
@@ -30,12 +30,16 @@ return [
     ],
     ],
 
 
     'stripe' => [
     'stripe' => [
-        'model'  => App\User::class,
-        'key'    => env('STRIPE_KEY'),
+        'model' => App\User::class,
+        'key' => env('STRIPE_KEY'),
         'secret' => env('STRIPE_SECRET'),
         'secret' => env('STRIPE_SECRET'),
     ],
     ],
 
 
     'expo' => [
     'expo' => [
         'access_token' => env('EXPO_ACCESS_TOKEN'),
         'access_token' => env('EXPO_ACCESS_TOKEN'),
     ],
     ],
+
+    'resend' => [
+        'key' => env('RESEND_KEY'),
+    ],
 ];
 ];

+ 95 - 30
resources/views/admin/messages/home.blade.php

@@ -1,36 +1,101 @@
 @extends('admin.partial.template-full')
 @extends('admin.partial.template-full')
 
 
 @section('section')
 @section('section')
-<div class="title">
-	<h3 class="font-weight-bold d-inline-block">Messages</h3>
 </div>
 </div>
+<div class="header bg-primary pb-3 mt-n4">
+    <div class="container-fluid">
+        <div class="header-body">
+            <div class="row align-items-center py-4">
+                <div class="col-lg-6 col-7">
+                    <p class="display-1 text-white d-inline-block mb-0">Messages</p>
+                </div>
+            </div>
+        </div>
+    </div>
+</div>
+<div class="container mt-3">
+
+    <div class="row justify-content-center">
+        @if (session('status'))
+        <div class="col-12" id="flash">
+            <div class="alert alert-success">
+                {{ session('status') }}
+            </div>
+        </div>
+        @endif
+        @if (session('error'))
+        <div class="col-12" id="flash">
+            <div class="alert alert-danger">
+                {{ session('error') }}
+            </div>
+        </div>
+        @endif
+        <div class="col-12">
+            <ul class="nav nav-pills my-3">
+                <li class="nav-item">
+                    <a class="nav-link {{$sort=='all'?'active':''}}" href="?sort=all">All</a>
+                </li>
+
+                <li class="nav-item">
+                    <a class="nav-link {{$sort=='open'?'active':''}}" href="?sort=open">Open</a>
+                </li>
 
 
-<hr>
-<div class="table-responsive">
-  <table class="table">
-    <thead class="bg-light">
-      <tr>
-        <th scope="col">#</th>
-        <th scope="col">User</th>
-        <th scope="col">Message</th>
-        <th scope="col">Created</th>
-      </tr>
-    </thead>
-    <tbody>
-      @foreach($messages as $msg)
-      <tr>
-        <td>
-          <a href="/i/admin/messages/show/{{$msg->id}}" class="btn btn-sm btn-outline-primary">
-           	{{$msg->id}}
-          </a>
-        </td>
-        <td class="font-weight-bold"><a href="{{$msg->user->url()}}">{{$msg->user->username}}</a></td>
-        <td class="font-weight-bold">{{str_limit($msg->message, 40)}}</td>
-        <td class="font-weight-bold">{{$msg->created_at->diffForHumans()}}</td>
-      </tr>
-      @endforeach
-    </tbody>
-  </table>
+                <li class="nav-item">
+                    <a class="nav-link {{$sort=='closed'?'active':''}}" href="?sort=closed">Closed</a>
+                </li>
+            </ul>
+        </div>
+        <div class="col-12">
+            <div class="table-responsive">
+              <table class="table">
+                <thead class="bg-light">
+                  <tr>
+                    <th scope="col">#</th>
+                    <th scope="col">User</th>
+                    <th scope="col">Message</th>
+                    <th scope="col">Created</th>
+                  </tr>
+                </thead>
+                <tbody>
+                  @foreach($messages as $msg)
+                  <tr>
+                    <td>
+                      <a href="/i/admin/messages/show/{{$msg->id}}" class="btn btn-sm btn-outline-primary">
+                       	{{$msg->id}}
+                      </a>
+                    </td>
+                    <td class="font-weight-bold"><a href="{{$msg->user->url()}}">{{$msg->user->username}}</a></td>
+                    <td class="font-weight-bold">{{str_limit($msg->message, 40)}}</td>
+                    <td class="font-weight-bold">{{$msg->created_at->diffForHumans()}}</td>
+                  </tr>
+                  @endforeach
+                </tbody>
+              </table>
+            </div>
+            <hr />
+            {{$messages->links()}}
+        </div>
+    </div>
 </div>
 </div>
-{{$messages->links()}}
-@endsection
+@endsection
+
+@push('scripts')
+<script type="text/javascript">
+    function checkAndRemoveElementOnLoad(selector, delay, action = 'hide') {
+        window.addEventListener('load', () => {
+            setTimeout(() => {
+                const element = document.querySelector(selector);
+                if (element) {
+                    if (action === 'hide') {
+                        element.style.display = 'none';
+                    } else if (action === 'remove') {
+                        element.remove();
+                    }
+                }
+            }, delay * 1000);
+        });
+    }
+
+    checkAndRemoveElementOnLoad('#flash', 5, 'remove');
+</script>
+@endpush

+ 170 - 39
resources/views/admin/messages/show.blade.php

@@ -4,56 +4,161 @@
 <div class="title">
 <div class="title">
 	<div class="d-flex justify-content-between align-items-center">
 	<div class="d-flex justify-content-between align-items-center">
 		<div class="font-weight-bold"># {{$message->id}}</div>
 		<div class="font-weight-bold"># {{$message->id}}</div>
-		<div class="font-weight-bold h3">Message</div>
-		<div>
-			@if($message->read_at)
-			<span class="btn btn-outline-secondary btn-sm disabled" disabled>Read</span>
-			@else
-			<button type="button" class="btn btn-outline-primary btn-sm" id="markRead">Mark Read</button>
-			@endif
-		</div>
+		<div class="font-weight-bold h3">Contact Form Message</div>
+		<div></div>
 	</div>
 	</div>
 </div>
 </div>
 
 
-<hr>
+<hr class="mt-0">
+
+<div class="row mb-3">
+	<div class="col-12 col-md-4">
+        <div class="card">
+            <div class="list-group list-group-flush">
+                @if($message->responded_at)
+                <div class="list-group-item">
+                    <div class="d-flex justify-content-between">
+                        <div class="small text-muted">Admin Response Sent</div>
+                        <div>
+                            <span class="font-weight-bold" title="{{$message->responded_at}}" data-toggle="tooltip">
+                                {{$message->responded_at->diffForHumans()}}
+                            </span>
+                        </div>
+                    </div>
+                </div>
+                @endif
+
+                <div class="list-group-item">
+                    <div class="d-flex justify-content-between">
+                        <div class="small text-muted">Status</div>
+                        @if($message->read_at == null)
+                        <div class="text-success font-weight-bold">Open</div>
+                        @else
+                        <div class="text-muted">Closed</div>
+                        @endif
+                    </div>
+                </div>
+                <div class="list-group-item">
+                    <div class="d-flex justify-content-between">
+                        <div class="small text-muted">Response Requested</div>
+                        @if($message->response_requested == 1)
+                        <div class="font-weight-bold">Yes</div>
+                        @else
+                        <div class="text-muted">No</div>
+                        @endif
+                    </div>
+                </div>
+
+                <div class="list-group-item">
+                    <div class="d-flex justify-content-between">
+                        <div class="small text-muted">Created</div>
+                        <div>
+                            <span class="font-weight-bold" title="{{$message->created_at}}" data-toggle="tooltip">
+                                {{$message->created_at->diffForHumans()}}
+                            </span>
+                        </div>
+                    </div>
+                </div>
+
+                @if($message->user && $message->user->last_active_at)
+                <div class="list-group-item">
+                    <div class="d-flex justify-content-between">
+                        <div class="small text-muted">User Last Active</div>
+                        <div>
+                            <span class="font-weight-bold" title="{{$message->user->last_active_at}}" data-toggle="tooltip">
+                                {{$message->user->last_active_at->diffForHumans()}}
+                            </span>
+                        </div>
+                    </div>
+                </div>
+                @endif
 
 
-<div class="row">
-	
-	<div class="col-12 col-md-3 text-md-right">
-		@if($message->response_requested)
-		<p class="text-dark font-weight-bold">Response Requested</p>
-		@endif
-		<p class="text-dark">Sent {{$message->created_at->diffForHumans()}}</p>
+                @if(!$message->read_at)
+                <div class="list-group-item">
+                    <button type="button" class="btn btn-outline-primary btn-block" id="markRead">Mark Read</button>
+                </div>
+                @endif
+            </div>
+        </div>
 	</div>
 	</div>
-	<div class="col-12 col-md-6">
-		
-		<div class="card shadow-none border">
-			<div class="card-header bg-white">
-				<div class="media">
-					<img src="{{$message->user->profile->avatarUrl()}}" class="mr-3 rounded-circle" width="40px" height="40px">
-					<div class="media-body">
-						<h5 class="my-0">&commat;{{$message->user->username}}</h5>
-						<span class="text-muted">{{$message->user->email}}</span>
+
+	<div class="col-12 col-md-8">
+		<div class="row">
+			<div class="col-12">
+				<div class="card shadow-none border">
+					<div class="card-header bg-white">
+						<div class="media">
+							<img
+                                src="{{$message->user->profile->avatarUrl()}}"
+                                class="mr-3 rounded-circle"
+                                width="40px"
+                                height="40px"
+                                onerror="this.onerror=null;this.src='/storage/avatars/default.png?v=0';">
+							<div class="media-body">
+								<h5 class="my-0">&commat;{{$message->user->username}}</h5>
+								<span class="text-muted">{{$message->user->email}}</span>
+							</div>
+						</div>
 					</div>
 					</div>
-				</div>
-			</div>
-			<div class="card-body">
-				<p class="mb-0">{{$message->message}}</p>
+					<div class="card-body">
+						<p class="text-uppercase text-muted small mb-2">Message Body</p>
+						<p class="mb-0">{{$message->message}}</p>
+
+                        <hr>
+                        <p class="text-uppercase text-muted small mb-2">Admin Reply:</p>
+
+                        @if($message->responded_at)
+                        <p class="mb-0">{{$message->response}}</p>
+                        @else
+                        @if(config('mail.default') === 'log')
+                        <div class="alert alert-danger">
+                        <p class="mb-0">You need to configure your mail driver before you can send outgoing emails.</p>
+                        </div>
+                        @else
+                        <form method="post" id="mform">
+    						@csrf
+    						<div class="form-group">
+    							<textarea
+                                    class="form-control"
+                                    name="message"
+                                    id="message"
+                                    rows="4"
+                                    style="resize: none;"
+                                    maxlength="500"
+                                    placeholder="Reply to &commat;{{$message->user->username}} via email ..."></textarea>
+    							@if ($errors->any())
+    							@foreach ($errors->all() as $error)
+    							<p class="invalid-feedback mb-0" style="display:block;">
+    								<strong>{{ $error }}</strong>
+    							</p>
+    							@endforeach
+    							@endif
+    						</div>
+    						<div class="d-flex justify-content-between align-items-center">
+                                <div>
+                                    <button type="button" class="btn btn-primary font-weight-bold submit-btn">Send</button>
+    								<button type="button" class="btn btn-outline-primary font-weight-bold preview-btn">Preview</button>
+                                </div>
+
+                                <div>
+        							<span class="small text-muted font-weight-bold">
+        								<span id="messageCount">0</span>/500
+        							</span>
+                                </div>
+    						</div>
+    					</form>
+                        @endif
+                        @endif
+    				</div>
+                </div>
 			</div>
 			</div>
 		</div>
 		</div>
 	</div>
 	</div>
-	<div class="col-12 col-md-3">
-		{{-- @if($message->responded_at == null)
-		<button class="btn btn-primary font-weight-bold btn-block">Send Response</button>
-		<hr>
-		@endif
-		<button class="btn btn-outline-danger font-weight-bold btn-block">Delete</button> --}}
-	</div>
 </div>
 </div>
-
 @endsection
 @endsection
 
 
 @push('scripts')
 @push('scripts')
+@if($message->responded_at == null)
 <script type="text/javascript">
 <script type="text/javascript">
 	$('#markRead').on('click', function(e) {
 	$('#markRead').on('click', function(e) {
 		e.preventDefault();
 		e.preventDefault();
@@ -61,8 +166,34 @@
 		axios.post('/i/admin/messages/mark-read', {
 		axios.post('/i/admin/messages/mark-read', {
 			id: '{{$message->id}}',
 			id: '{{$message->id}}',
 		}).then(res => {
 		}).then(res => {
-			window.location.href = window.location.href;
+			window.location.href = '/i/admin/messages/home';
 		})	
 		})	
 	})
 	})
+
+    const submitBtn = document.querySelector('.submit-btn');
+    submitBtn.addEventListener('click', () => {
+        const form = document.getElementById('mform');
+        form.action = '/i/admin/messages/show/{{$message->id}}';
+        form.submit()
+    });
+
+    const previewBtn = document.querySelector('.preview-btn');
+    previewBtn.addEventListener('click', () => {
+        const form = document.getElementById('mform');
+        form.action = '/i/admin/messages/preview/{{$message->id}}';
+        form.submit()
+    });
+
+    function countChars() {
+        const input = document.getElementById('message');
+        const counter = document.getElementById('messageCount');
+
+        input.addEventListener('input', function() {
+            counter.textContent = input.value.length;
+        });
+    }
+
+    countChars();
 </script>
 </script>
-@endpush
+@endif
+@endpush

+ 24 - 0
resources/views/emails/contact/admin-response.blade.php

@@ -0,0 +1,24 @@
+<x-mail::message>
+Hello **&commat;{{$contact->user->username}}**,
+
+You contacted the admin team of {{config('pixelfed.domain.app')}} with the following inquiry:
+
+<x-mail::panel>
+<i>{{str_limit($contact->message, 80)}}</i>
+</x-mail::panel>
+
+<x-mail::button :url="$url" color="primary">
+    View Admin Response
+</x-mail::button>
+
+<small>
+or copy and paste the following url: <a href="{{$url}}">{{$url}}</a>
+</small>
+<br>
+<br>
+<br>
+<small>
+Thanks,<br>
+The {{ ucfirst(config('pixelfed.domain.app')) }} Admin Team
+</small>
+</x-mail::message>

+ 55 - 0
resources/views/site/contact/admin-response.blade.php

@@ -0,0 +1,55 @@
+@extends('layouts.blank')
+
+@section('content')
+<div class="container pt-5">
+    <div class="row justify-content-center">
+        <div class="col-12 col-lg-8">
+            <div class="card shadow-none border">
+                <div class="card-header d-flex justify-content-between align-items-center">
+                    <a class="btn btn-link font-weight-bold" href="/i/web">Back to Pixelfed</a>
+                    <h1 class="h4 mb-0">Contact Form Response</h1>
+                    <p class="d-none d-md-block mb-0 text-muted">Ticket ID #{{$contact->id}}</p>
+                </div>
+
+                <div class="list-group list-group-flush">
+                    <div class="list-group-item">
+                        <div class="media">
+                            <img src="{{$contact->user->profile->avatarUrl()}}" class="mr-3 rounded-circle" width="40px" height="40px" onerror="this.onerror=null;this.src='/storage/avatars/default.png?v=2'">
+                            <div class="media-body">
+                                <h5 class="my-0">&commat;{{$contact->user->username}}</h5>
+                                <span class="text-muted">{{$contact->user->name}}</span>
+                            </div>
+                        </div>
+                        <p class="my-2 font-weight-bold">You sent the following inquiry:</p>
+                        <div class="card shadow-none border bg-light rounded mb-2">
+                            <div class="card-body">
+                                {{ $contact->message }}
+                            </div>
+                        </div>
+                        <p class="small text-muted">You sent this inquiry on {{$contact->created_at->format('M d, Y')}} at {{$contact->created_at->format('h:i:s a e')}}</p>
+                    </div>
+
+                    @if($contact->response)
+                    <div class="list-group-item">
+                        <p class="my-2 font-weight-bold">The admin(s) responded to your inquiry:</p>
+                        <div class="card shadow-none border bg-light rounded mb-2">
+                            <div class="card-body">
+                                {{ $contact->response }}
+                            </div>
+                        </div>
+                        @if($contact->responded_at)
+                        <p class="small text-muted">The response was created on {{$contact->responded_at->format('M d, Y')}} at {{$contact->responded_at->format('h:i:s a e')}}</p>
+                        @endif
+                    </div>
+                    <div class="list-group-item">
+                        <div class="text-center">
+                            <p class="mb-0 small text-muted font-weight-bold">If you would like to respond, use the <a href="/site/contact">contact form</a>.</p>
+                        </div>
+                    </div>
+                    @endif
+                </div>
+            </div>
+        </div>
+    </div>
+</div>
+@endsection

+ 2 - 0
routes/web-admin.php

@@ -77,6 +77,8 @@ Route::domain(config('pixelfed.domain.admin'))->prefix('i/admin')->group(functio
     Route::get('messages/home', 'AdminController@messagesHome')->name('admin.messages');
     Route::get('messages/home', 'AdminController@messagesHome')->name('admin.messages');
     Route::get('messages/show/{id}', 'AdminController@messagesShow');
     Route::get('messages/show/{id}', 'AdminController@messagesShow');
     Route::post('messages/mark-read', 'AdminController@messagesMarkRead');
     Route::post('messages/mark-read', 'AdminController@messagesMarkRead');
+    Route::post('messages/show/{id}', 'AdminController@messagesReply');
+    Route::post('messages/preview/{id}', 'AdminController@messagesReplyPreview');
     Route::redirect('site-news', '/i/admin/newsroom');
     Route::redirect('site-news', '/i/admin/newsroom');
     Route::get('newsroom', 'AdminController@newsroomHome')->name('admin.newsroom.home');
     Route::get('newsroom', 'AdminController@newsroomHome')->name('admin.newsroom.home');
     Route::get('newsroom/create', 'AdminController@newsroomCreate')->name('admin.newsroom.create');
     Route::get('newsroom/create', 'AdminController@newsroomCreate')->name('admin.newsroom.create');

+ 2 - 1
routes/web.php

@@ -125,7 +125,8 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact
 
 
         Route::get('warning', 'AccountInterstitialController@get');
         Route::get('warning', 'AccountInterstitialController@get');
         Route::post('warning', 'AccountInterstitialController@read');
         Route::post('warning', 'AccountInterstitialController@read');
-        Route::get('my2020', 'SeasonalController@yearInReview');
+
+        Route::get('contact-admin-response/{id}', 'ContactController@showAdminResponse');
 
 
         Route::get('web/my-portfolio', 'PortfolioController@myRedirect');
         Route::get('web/my-portfolio', 'PortfolioController@myRedirect');
         Route::get('web/hashtag/{tag}', 'SpaController@hashtagRedirect');
         Route::get('web/hashtag/{tag}', 'SpaController@hashtagRedirect');