Browse Source

Admin messages

Samson Mutunga 1 year ago
parent
commit
28e43947fd

+ 4 - 0
.env.example

@@ -68,3 +68,7 @@ FACEBOOK_CLIENT_ID=721191675861105
 FACEBOOK_CLIENT_SECRET=7e0f69cf629484a624a5abb4e3f3c93d
 
 INTERCOM_API_SECRET=dG9rOmNkZDFkMWI3X2MwODlfNDAxNV9hYzM5X2Y5YTIwOGU5MjFmYzoxOjA=
+
+OPENTOK_API_KEY=46871644
+OPENTOK_API_SECRET=48c39d640cbcfb1032606d7c40ab5971290a5163
+OPENTOK_SESSION_ID=1_MX40Njg3MTY0NH5-MTU5NjQyMzcxMjQ4OX5PRnNIVmFDU2t2d3BnWG1YbkMvSWFRNk1-fg

+ 80 - 0
app/Helpers.php

@@ -182,5 +182,85 @@ if (!function_exists('days_until_this_date')) {
         return $days . ' days';
     }
 }
+if(!function_exists('friendly_date_est')) {
+    function friendly_date_est($value, $default = '-') {
 
+        if(!$value || empty($value)) return $default;
+        try {
+
+            $realTimezone = resolve_timezone('EASTERN');
+            $date = new DateTime($value);
+            $date->setTimezone(new DateTimeZone($realTimezone));
+
+            return $date->format("m/d/y");
+
+        }
+        catch (Exception $e) {
+            return $e->getMessage();
+        }
+    }
+}
+
+if(!function_exists('resolve_timezone')) {
+    function resolve_timezone($value) {
+        try {
+            switch ($value) {
+                case 'ALASKA': {
+                    return 'US/Alaska';
+                }
+                case 'CENTRAL': {
+                    return 'US/Central';
+                }
+                case 'EASTERN': {
+                    return 'US/Eastern';
+                }
+                case 'HAWAII': {
+                    return 'US/Hawaii';
+                }
+                case 'MOUNTAIN': {
+                    return 'US/Mountain';
+                }
+                case 'PACIFIC': {
+                    return 'US/Pacific';
+                }
+                case 'PUERTO_RICO': {
+                    return 'America/Puerto_Rico';
+                }
+                case 'UTC': {
+                    return 'UTC';
+                }
+                }
+        }
+        catch (Exception $e) {
+            return $value;
+        }
+    }
+}
+
+if(!function_exists('friendly_time_by_time_zone')) {
+    function friendly_time_by_time_zone($value, $default = '-', $fromTz = 'UCT', $toTz = 'US/Eastern') {
+        if(!$value || empty($value)) return $default;
+        try {
+            $date = new DateTime($value, new DateTimeZone($fromTz));
+            $date->setTimezone(new DateTimeZone($toTz));
+            return $date->format('h:i a') . ' ' . friendly_timezone($toTz);
+        }
+        catch (Exception $e) {
+            return $value;
+        }
+    }
+}
+if(!function_exists('friendly_timezone')) {
+    function friendly_timezone($value) {
+        try {
+            switch ($value) {
+                case 'US/Eastern': {
+                    return 'EST';
+                }
+            }
+        }catch(Exception $e){
+            return $value;
+        }
+    }
+}
 ?>

+ 39 - 0
app/Http/Controllers/AdminController.php

@@ -12,8 +12,11 @@ use App\Models\StoreOrder;
 use App\Http\Services\EmailService;
 use App\Models\AppSession;
 use App\Models\AppUserTransaction;
+use App\Models\Message;
 use App\Models\UserEvent;
 use Barryvdh\DomPDF\Facade\Pdf;
+use OpenTok\MediaMode;
+use OpenTok\OpenTok;
 
 class AdminController extends Controller
 {
@@ -262,4 +265,40 @@ class AdminController extends Controller
         }
         return $this->pass();
     }
+
+    public function userMessages(Request $request, User $user){
+        $messages = Message::where('to_user_id', $user->id)->orWhere('from_user_id', $user->id);
+        $messages = $messages->orderBy('created_at', 'ASC')->paginate(25);
+
+        $opentok = null;
+        $otSession = null;
+        $otSessionId = null;
+        $otToken = null;
+
+        try {
+            $opentok = new OpenTok(config('app.opentokApiKey'), config('app.opentokApiSecret'));
+            $otSession = $opentok->createSession(array('mediaMode' => MediaMode::ROUTED));
+            $otSessionId = $otSession->getSessionId();
+            $otToken = $opentok->generateToken($otSessionId);
+          } catch (\Exception $e) {
+            return redirect()->back()->with('error', 'OpenTok issue!');
+          }
+
+          $toUser = $user;
+
+        return view('app.my-account.admin.users.sub.messages', compact('otSessionId', 'otToken', 'messages', 'toUser'));
+    }
+    public function userMessageAttachments(Request $request, User $user, Message $message){
+        if(!$message) return '';
+        $output = [];
+        foreach ($message->attachments as $attachment) {
+            $output[] = '<a native target="_blank" ' .
+                'href="/api/systemFile/view/' . $attachment->systemFile->uid . '" ' .
+                'class="attachment text-sm my-1">' .
+                '<i class="fa fa-paperclip"></i>&nbsp;' .
+                $attachment->systemFile->file_name .
+                '</a>';
+        }
+        return implode("", $output);
+    }
 }

+ 26 - 0
app/Models/Message.php

@@ -0,0 +1,26 @@
+<?php
+
+namespace App\Models;
+
+use Illuminate\Database\Eloquent\Factories\HasFactory;
+use Illuminate\Database\Eloquent\Model;
+
+class Message extends BaseModel
+{
+    use HasFactory;
+
+    protected $table = 'message';
+
+    public function fromUser(){
+        return $this->hasOne(User::class, 'id', 'from_user_id');
+    }
+
+    public function toUser(){
+        return $this->hasOne(User::class, 'id', 'to_user_id');
+    }
+
+    public function attachments(){
+        return $this->hasMany(MessageAttachment::class, 'message_id', 'id');
+    }
+
+}

+ 21 - 0
app/Models/MessageAttachment.php

@@ -0,0 +1,21 @@
+<?php
+
+namespace App\Models;
+
+use Illuminate\Database\Eloquent\Factories\HasFactory;
+use Illuminate\Database\Eloquent\Model;
+
+class MessageAttachment extends BaseModel
+{
+    use HasFactory;
+
+    protected $table = 'message_attachment';
+
+    public function message(){
+        return $this->hasOne(Message::class, 'id', 'message_id');
+    }
+
+    public function systemFile(){
+        return $this->hasOne(SystemFile::class, 'id', 'attachment_id');
+    }
+}

+ 13 - 0
app/Models/SystemFile.php

@@ -0,0 +1,13 @@
+<?php
+
+namespace App\Models;
+
+use Illuminate\Database\Eloquent\Factories\HasFactory;
+use Illuminate\Database\Eloquent\Model;
+
+class SystemFile extends BaseModel
+{
+    use HasFactory;
+
+    protected $table = 'system_file';
+}

+ 2 - 1
composer.json

@@ -13,7 +13,8 @@
         "laravel/framework": "^8.75",
         "laravel/sanctum": "^2.11",
         "laravel/socialite": "^5.6",
-        "laravel/tinker": "^2.5"
+        "laravel/tinker": "^2.5",
+        "opentok/opentok": "^4.14"
     },
     "require-dev": {
         "facade/ignition": "^2.5",

+ 186 - 1
composer.lock

@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
     ],
-    "content-hash": "b560e221d97a5b3199b5770fee5c3c4b",
+    "content-hash": "5942891dd1403a5f408d564491eb390a",
     "packages": [
         {
             "name": "asm89/stack-cors",
@@ -703,6 +703,69 @@
             ],
             "time": "2020-12-29T14:50:06+00:00"
         },
+        {
+            "name": "firebase/php-jwt",
+            "version": "v6.9.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/firebase/php-jwt.git",
+                "reference": "f03270e63eaccf3019ef0f32849c497385774e11"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/firebase/php-jwt/zipball/f03270e63eaccf3019ef0f32849c497385774e11",
+                "reference": "f03270e63eaccf3019ef0f32849c497385774e11",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.4||^8.0"
+            },
+            "require-dev": {
+                "guzzlehttp/guzzle": "^6.5||^7.4",
+                "phpspec/prophecy-phpunit": "^2.0",
+                "phpunit/phpunit": "^9.5",
+                "psr/cache": "^1.0||^2.0",
+                "psr/http-client": "^1.0",
+                "psr/http-factory": "^1.0"
+            },
+            "suggest": {
+                "ext-sodium": "Support EdDSA (Ed25519) signatures",
+                "paragonie/sodium_compat": "Support EdDSA (Ed25519) signatures when libsodium is not present"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Firebase\\JWT\\": "src"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Neuman Vong",
+                    "email": "neuman+pear@twilio.com",
+                    "role": "Developer"
+                },
+                {
+                    "name": "Anant Narayanan",
+                    "email": "anant@php.net",
+                    "role": "Developer"
+                }
+            ],
+            "description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.",
+            "homepage": "https://github.com/firebase/php-jwt",
+            "keywords": [
+                "jwt",
+                "php"
+            ],
+            "support": {
+                "issues": "https://github.com/firebase/php-jwt/issues",
+                "source": "https://github.com/firebase/php-jwt/tree/v6.9.0"
+            },
+            "time": "2023-10-05T00:24:42+00:00"
+        },
         {
             "name": "fruitcake/laravel-cors",
             "version": "v2.2.0",
@@ -1173,6 +1236,57 @@
             ],
             "time": "2023-04-17T16:11:26+00:00"
         },
+        {
+            "name": "johnstevenson/json-works",
+            "version": "v1.1.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/johnstevenson/json-works.git",
+                "reference": "97eca2c9956894374d41dcaf8031d123a8705100"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/johnstevenson/json-works/zipball/97eca2c9956894374d41dcaf8031d123a8705100",
+                "reference": "97eca2c9956894374d41dcaf8031d123a8705100",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3.3"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "4.*",
+                "squizlabs/php_codesniffer": "2.*"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "JohnStevenson\\JsonWorks\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "John Stevenson",
+                    "email": "john-stevenson@blueyonder.co.uk"
+                }
+            ],
+            "description": "Create, edit, query and validate json",
+            "homepage": "http://github.com/johnstevenson/json-works",
+            "keywords": [
+                "builder",
+                "json",
+                "schema",
+                "validator"
+            ],
+            "support": {
+                "issues": "https://github.com/johnstevenson/json-works/issues",
+                "source": "https://github.com/johnstevenson/json-works/tree/master"
+            },
+            "time": "2016-01-05T16:23:18+00:00"
+        },
         {
             "name": "laravel/framework",
             "version": "v8.83.27",
@@ -2497,6 +2611,77 @@
             },
             "time": "2023-03-05T19:49:14+00:00"
         },
+        {
+            "name": "opentok/opentok",
+            "version": "v4.14.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/opentok/OpenTok-PHP-SDK.git",
+                "reference": "3fe0e1784a28d8d57625a1c6daa45079e9de3293"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/opentok/OpenTok-PHP-SDK/zipball/3fe0e1784a28d8d57625a1c6daa45079e9de3293",
+                "reference": "3fe0e1784a28d8d57625a1c6daa45079e9de3293",
+                "shasum": ""
+            },
+            "require": {
+                "ext-json": "*",
+                "ext-xml": "*",
+                "firebase/php-jwt": "^6.0",
+                "guzzlehttp/guzzle": "~6.0|~7.0",
+                "johnstevenson/json-works": "~1.1",
+                "php": "^7.2|^8.0"
+            },
+            "require-dev": {
+                "helmich/phpunit-json-assert": "^3.0.0",
+                "phing/phing": "~2.16.0",
+                "php-http/guzzle7-adapter": "^1.0",
+                "php-http/mock-client": "^1.4",
+                "phpstan/phpstan": "^0.12",
+                "phpunit/phpunit": "^7.4|^8.0",
+                "rector/rector": "^0.8",
+                "squizlabs/php_codesniffer": "^3.1"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "OpenTok\\": "src/OpenTok",
+                    "OpenTokTest\\": "tests/OpenTokTest"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Ankur Oberoi",
+                    "email": "ankur@tokbox.com",
+                    "role": "Developer"
+                },
+                {
+                    "name": "Community contributors",
+                    "homepage": "https://github.com/opentok/Opentok-PHP-SDK/graphs/contributors"
+                }
+            ],
+            "description": "OpenTok is a platform for creating real time streaming video applications, created by TokBox.",
+            "homepage": "https://github.com/opentok/Opentok-PHP-SDK",
+            "keywords": [
+                "OpenTok",
+                "TokBox",
+                "WebRTC",
+                "php",
+                "streaming",
+                "video"
+            ],
+            "support": {
+                "email": "support@tokbox.com",
+                "issues": "https://github.com/opentok/Opentok-PHP-SDK/issues",
+                "source": "https://github.com/opentok/OpenTok-PHP-SDK/tree/v4.14.1"
+            },
+            "time": "2023-09-25T10:43:18+00:00"
+        },
         {
             "name": "opis/closure",
             "version": "3.6.3",

+ 4 - 0
config/app.php

@@ -32,6 +32,10 @@ return [
     'orderAppUrl' => env('ORDER_APP_URL'),
     'intercomApiSecret' => env('INTERCOM_API_SECRET'),
 
+    'opentokApiKey' => env('OPENTOK_API_KEY'),
+    'opentokApiSecret' => env('OPENTOK_API_SECRET'),
+    'opentokSessionId' => env('OPENTOK_SESSION_ID'),
+
     /*
     |--------------------------------------------------------------------------
     | Application Environment

+ 331 - 0
public/css/messages.css

@@ -0,0 +1,331 @@
+.position-relative {
+  position: relative;
+}
+.overflow-hidden {
+  overflow: hidden;
+}
+.position-absolute {
+  position: absolute;
+}
+.opacity-0x {
+  opacity: 0;
+}
+.im-body {
+  height: calc(80vh - 185px) !important;
+}
+.im-container {
+  display: flex;
+  align-items: stretch;
+}
+.im-container .im-lhs {
+  min-width: 300px;
+  border-right: 1px solid #ddd;
+  padding: 0.5rem;
+  overflow: auto;
+}
+.im-container .im-lhs .im-conversation {
+  border-radius: 4px;
+  padding: 0.5rem;
+  border: 1px solid #ddd;
+  background: white;
+  cursor: pointer;
+}
+.im-container .im-lhs .im-conversation:hover {
+  background: aliceblue;
+  text-decoration: none;
+}
+.im-container .im-lhs .im-conversation.active {
+  background: aliceblue;
+  border-color: #307899;
+}
+.im-container .im-lhs .im-conversation:not(:last-child) {
+  margin-bottom: 0.5rem;
+}
+.im-container .im-lhs .im-conversation .unread-conversation {
+  position: absolute;
+  right: 0;
+  top: 0;
+}
+.im-container .im-rhs {
+  flex-grow: 1;
+  display: inline-flex;
+  flex-direction: column;
+}
+.im-container .im-rhs .im-messages {
+  flex-grow: 1;
+  display: inline-flex;
+  flex-direction: column;
+  justify-content: flex-end;
+  overflow: auto;
+  margin-bottom: 1rem;
+}
+.im-container .im-message {
+  margin-top: 0.5rem;
+  padding: 0 0.5rem;
+  text-align: left;
+}
+.im-container .im-message.sent {
+  text-align: right;
+}
+.im-container .im-rhs .im-messages-header {
+  padding-bottom: 0.5rem;
+  border-bottom: 1px solid #eee;
+}
+.im-container .im-message .im-message-sender {
+  display: flex;
+  justify-content: flex-start;
+}
+.im-container .im-message .im-message-sender .unread-message {
+  margin-bottom: -5px;
+  margin-top: -5px;
+  font-size: 16px;
+}
+.im-container .im-message .im-message-content {
+  background: #eee;
+  border-radius: 6px;
+  padding: 0.5rem;
+  margin-top: 0.25rem;
+  /* white-space: pre-wrap; */
+  word-break: break-word;
+  text-align: left;
+  display: inline-block;
+  min-width: 150px;
+  max-width: 75%;
+  flex-grow: 0;
+  margin-right: 5px;
+}
+.im-container .im-message.sent .im-message-sender {
+  justify-content: flex-end;
+}
+.im-container .im-message.sent .im-message-content {
+  margin-left: auto;
+  background: #e4f9ff;
+}
+
+.im-container .im-rhs .im-input textarea {
+  width: 100%;
+  height: 35px;
+  border:none;
+  resize:none;
+  padding: 0.3rem;
+  box-shadow: none !important;
+}
+.im-container .im-rhs .im-input textarea:focus,
+.im-container .im-rhs .im-input textarea:active,
+.im-container .im-rhs .im-input textarea:focus-visible {
+  border:none;
+  outline: 0;
+}
+.im-container .im-rhs .im-input #selected-files .selected-file {
+  background: #eee;
+  border: 1px solid #ccc;
+  border-radius: 3px;
+  padding: 2px 20px 2px 5px;
+  cursor: pointer;
+  position: relative;
+  margin-top: 6px;
+  margin-right: 6px;
+}
+.im-container .im-rhs .im-input #selected-files .selected-file:hover {
+  color: #b11313;
+}
+.im-container .im-rhs .im-input #selected-files .selected-file:after {
+  position: absolute;
+  content: '✕';
+  right: 4px;
+  color: #b11313;
+}
+.im-container .im-message.sent .header-item.text-sm {
+  margin-right: 5px;
+}
+.im-container .im-message.sent .header-item {
+  margin-left: 0.75rem;
+}
+.im-container .im-message.received .header-item {
+  margin-right: 0.75rem;
+}
+.im-container .im-message.sent .attachment {
+  margin-left: 0.75rem;
+}
+.im-container .im-message.received .attachment {
+  margin-right: 0.75rem;
+}
+.im-container .im-rhs .im-video-container {
+  max-width: 480px;
+  display: inline-block;
+  position: relative;
+  margin-top: 4px;
+}
+.im-container .im-rhs .im-video-container video {
+  max-width: 100%;
+}
+.im-container .im-rhs .im-video-container .vs-control {
+  opacity: 0;
+  transition: opacity 0.3s ease;
+  position: absolute;
+  margin: 0 !important;
+  top: 0;
+  right: 0;
+  background: rgba(238, 238, 238, 0.75) !important;
+  border: 0 !important;
+  border-bottom-left-radius: 3px;
+  text-align: right !important;
+  width: auto !important;
+  font-size: 11px !important;
+}
+.im-container .im-rhs .im-video-container:hover .vs-control {
+  opacity: 1;
+  background: rgba(238, 238, 238, 1) !important;
+}
+.im-container .im-rhs .im-video-container .vs-control select {
+  width: auto !important;
+  border: 0 !important;
+}
+.msg-header {
+  background-color: #fefefe;
+  color: #333;
+  border-bottom: 1px solid #ccc;
+}
+.im-body .card-body {
+  background-color: #fefefe;
+}
+.msg-input {
+  border: 1px solid #eee;
+  padding: 8px 10px;
+  border-radius: 50px;
+  background-color: #ffffff;
+  width: 100%;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+}
+.msg-input textarea, .msg-input textarea:focus{
+  outline:0;
+  box-shadow: 0;
+  border:none;
+  width: 100%;
+}
+.btn-input, .btn-input:hover, .btn-input:focus {
+  height: 40px;
+  width: 40px;
+  font-size: 18px;
+  display: grid;
+  place-items:center;
+  border-radius: 100%;
+  background: #0367b0;
+  color: #fff;
+}
+.btn-input i {
+  margin-left: -4px;
+}
+.plus, .plus:hover, .plus:focus {
+  outline:0;
+  border:0;
+  background: 0;
+  color: #aaa;
+  margin-right: 5px;
+}
+.fabs {
+  display: flex;
+  gap: 5px;
+}
+.fabs .btn {
+  margin-left: 0 !important;
+  margin-bottom: 10px;
+  height: 40px;
+  width: 40px;
+  border-radius: 100%;
+}
+.msg-list {
+  height: calc(100vh - 185px) !important;
+  overflow-y: auto;
+}
+.msg-list .msg-header {
+  position: sticky;
+  top: 0;
+}
+.pt {
+  color: #000000;
+}
+.active-pt, .pt:hover {
+  background-color: #c2efff;
+  color: #000000;
+  transition: .5s;
+}
+.im-container .im-message .im-message-sender {
+  position: relative;
+}
+
+.im-container .im-message .delete,
+.im-container .im-message .edit {
+  position: absolute;
+  right: -14px;
+  top: 0px;
+}
+
+.im-container .im-message .edit {
+  right: -18px;
+  top: 25px;
+}
+
+.im-container .im-message:hover .edit,
+.im-container .im-message:focus .edit,
+.im-container .im-message:hover .delete,
+.im-container .im-message:focus .delete {
+  display: block;
+}
+.circle-icon {
+  margin-top: 5px;
+  height: 40px;
+  width: 40px;
+  text-align: center;
+  border-radius: 100%;
+  background-color: #ededed;
+  font-size: 15px;
+  font-weight: bold;
+  display: grid;
+  color: #444;
+  place-items: center;
+}
+.circle-icon.op  {
+  opacity: 0;
+  height: 10px;
+}
+
+.sidebar a {
+  display: block;
+  padding: 8px 12px;
+  color: #000000;
+  border-bottom: 1px solid #ddd;
+  font-size: 15px;
+}
+
+.sidebar a:last-child {
+  border-bottom: 0;
+}
+
+.sidebar .active,
+.sidebar a:hover {
+  color:var(--pry-color);
+  border-bottom-color: var(--pry-color);
+  transition: .5s;
+}
+.custom-menu {
+  transform: none !important;
+  top: 30px !important;
+}
+
+.custom-menu .dropdown-item {
+  padding: 10px !important;
+  border-bottom: 1px solid #eee;
+}
+
+.table *{
+  font-size: 14px !important;
+}
+
+@media screen and (max-width:500px) {
+  .fabs {
+    display: block;
+  }
+}

+ 2 - 0
resources/views/app/my-account/admin/users/partials/sidebar-mobile.blade.php

@@ -6,3 +6,5 @@
 
 <a class="d-block text-nowrap {{\Illuminate\Support\Facades\Route::getCurrentRoute()->getName() === 'admin.users.view.payment-methods' ? 'fw-bold' : ''}}"
    href="{{ route('admin.users.view.payment-methods', $user) }}">Payment Methods</a>
+   <a class="d-block text-nowrap {{\Illuminate\Support\Facades\Route::getCurrentRoute()->getName() === 'admin.users.view.messages' ? 'fw-bold' : ''}}"
+   href="{{ route('admin.users.view.messages', $user) }}">Messages</a>

+ 4 - 0
resources/views/app/my-account/admin/users/partials/sidebar.blade.php

@@ -38,6 +38,10 @@ $currentMenu = \Illuminate\Support\Facades\Request::segment(4);
                 <a class="nav-link {{\Illuminate\Support\Facades\Route::getCurrentRoute()->getName() === 'admin.users.view.payment-methods' ? 'active' : ''}}"
                    href="{{ route('admin.users.view.payment-methods', $user) }}">Payment Methods</a>
             </li>
+            <li class="nav-item ps-2">
+                <a class="nav-link {{\Illuminate\Support\Facades\Route::getCurrentRoute()->getName() === 'admin.users.view.messages' ? 'active' : ''}}"
+                   href="{{ route('admin.users.view.messages', $user) }}">Messages</a>
+            </li>
             <li class="nav-item ps-2">
                 <a class="nav-link {{\Illuminate\Support\Facades\Route::getCurrentRoute()->getName() === 'admin.users.view.user-events' ? 'active' : ''}}"
                    href="{{ route('admin.users.view.user-events', $user) }}">User Events</a>

+ 348 - 0
resources/views/app/my-account/admin/users/sub/messages.blade.php

@@ -0,0 +1,348 @@
+@extends('app.my-account.admin.users.single')
+
+@section('details')
+
+    <div>
+        <h4><b>Messages</b></h4>
+        <script src="https://static.opentok.com/v2/js/opentok.min.js"></script>
+        <div class="container">
+            <div class="custom-card w-100 p-0 patients-content">
+                <div class="mcp-theme-1 im-body" id="messages">
+                    <div class="card overflow-hidden border-0 h-100">
+
+                        <div class="msg-header p-3 d-flex align-items-center shadow-sm">
+                            <strong class="">
+                                <i class="fas fa-comments mr-2"></i>
+                                Messages
+                            </strong>
+                        </div>
+                        <div class="card-body p-0 h-100">
+
+                            <div class="im-container h-100">
+                                <div class="im-rhs p-2">
+                                    @if (!$messages || !count($messages))
+                                        <div class="py-4 text-center text-secondary">
+                                            <h3 class="mb-0"><i class="far fa-comment-dots"></i></h3>
+                                            No messages yet!
+                                        </div>
+                                    @endif
+                                    <div class="im-messages">
+                                        <div class="mh-100 overflow-auto opacity-0" id="im-scroller">
+                                            <?php $messageDates = []; ?>
+                                            @foreach ($messages as $k => $message)
+                                                @if (!in_array(friendly_date($message->created_at), $messageDates))
+                                                    <?php array_push($messageDates, friendly_date($message->created_at)); ?>
+                                                    <div class="text-center my-4">
+                                                        <span style="background:#c2efff"
+                                                            class="py-1 px-3 text-secondary rounded text-sm">{{ friendly_date_est($message->created_at) }}</span>
+                                                    </div>
+                                                @endif
+                                                <div class="d-flex align-items-start">
+                                                    @if ($message->from_user_id !== $performer->user->id)
+                                                        <div class="">
+                                                            @if (!$message->is_removed)
+                                                                @if ($k == 0 || $messages[$k - 1]->from_user_id !== $messages[$k]->from_user_id)
+                                                                    <!-- remove unecessary timestamps -->
+                                                                    <div class="circle-icon">
+                                                                        {{ mb_substr($message->fromUser->name_first, 0, 1) }}
+                                                                    </div>
+                                                                @else
+                                                                    <div class="circle-icon op">
+                                                                        {{ mb_substr($message->fromUser->name_first, 0, 1) }}
+                                                                    </div>
+                                                                @endif
+                                                            @endif
+                                                        </div>
+                                                    @endif
+                                                    <div class="w-100">
+                                                        <div class="im-message mt-0 pr-5 {{ $message->from_user_id === $performer->user->id ? 'sent' : 'received' }}"
+                                                            data-uid="{{ $message->uid }}"
+                                                            data-mark-as-read="{{ !$message->is_read ? 1 : 0 }}">
+                                                            <div class="im-message-sender align-items-center">
+                                                                @if (!$message->is_removed)
+                                                                    @if ($k == 0 || $messages[$k - 1]->from_user_id !== $messages[$k]->from_user_id)
+                                                                        <!-- remove unecessary timestamps -->
+                                                                        @if ($message->from_user_id !== $performer->user->id)
+                                                                            <small
+                                                                                class="mr-2 text-secondary">{{ $message->fromUser->displayName() }},</small>
+                                                                        @endif
+                                                                        <small
+                                                                            class="header-item text-secondary text-sm">{{ friendly_time_by_time_zone($message->created_at) }}</small>
+                                                                    @endif
+                                                                    @if ($performer->user->id == $message->created_by_user_id && $message->content_text)
+                                                                        <div class="header-item">
+                                                                            <div moe large relative>
+                                                                                @if ($k == 0 || $messages[$k - 1]->from_user_id !== $messages[$k]->from_user_id)
+                                                                                    <a href="#" start show><i
+                                                                                            class="fa fa-edit on-hover-opaque text-pry text-sm hidden edit"
+                                                                                            style="top:18px;"></i></a>
+                                                                                @else
+                                                                                    <a href="#" start show><i
+                                                                                            class="fa fa-edit on-hover-opaque text-pry text-sm hidden edit"></i></a>
+                                                                                @endif
+                                                                                <form url="/api/message/editContent"
+                                                                                    class="text-left" right>
+                                                                                    <input type="hidden" name="uid"
+                                                                                        value="{{ $message->uid }}">
+                                                                                    <div class="mb-2">
+                                                                                        <label class="mb-1 text-sm">Edit
+                                                                                            Message</label>
+                                                                                        <textarea name="contentText" rows="3" class="form-control form-control-sm">{{ $message->content_text }}</textarea>
+                                                                                    </div>
+                                                                                    <div class="mt-3">
+                                                                                        <button submit
+                                                                                            class="btn btn-sm btn-primary mr-2">Submit</button>
+                                                                                        <button cancel
+                                                                                            class="btn btn-default btn-sm border">Cancel</button>
+                                                                                    </div>
+                                                                                </form>
+                                                                            </div>
+                                                                        </div>
+                                                                    @endif
+                                                                    @if ($message->from_user_id === $performer->user->id)
+                                                                        <span class="header-item hidden delete">
+                                                                            <a href="#" native class="remove-message"
+                                                                                data-message-uid="{{ $message->uid }}"><i
+                                                                                    class="fa fa-trash-alt text-danger text-sm"></i></a>
+                                                                        </span>
+                                                                    @endif
+                                                                @endif
+                                                            </div>
+                                                            @if ($message->is_removed)
+                                                                <div class="im-message-content text-secondary font-italic">
+                                                                    This message was removed.</div>
+                                                            @else
+                                                                @if ($message->content_text)
+                                                                    <?php
+                                                                    $message->content_text = preg_replace_callback(
+                                                                        '~(?:http|ftp)s?://(?:www\.)?([a-z0-9.-]+\.[a-z]{2,3}(?:/\S*)?)~i',
+                                                                        function ($match) {
+                                                                            return '<a native target="_blank" href="' . $match[0] . '">' . $match[0] . '</a>';
+                                                                        },
+                                                                        $message->content_text,
+                                                                    );
+                                                                    ?>
+                                                                    <div class="im-message-content">{!! $message->content_text !!}
+                                                                    </div>
+                                                                @endif
+                                                            @endif
+                                                            @if (count($message->attachments) && !$message->is_removed)
+                                                                <div class="attachments-container mt-1 d-flex align-items-center flex-wrap {{ $message->from_user_id === $performer->user->id ? 'justify-content-end' : 'justify-content-start' }}"
+                                                                    data-message-uid="{{ $message->uid }}"
+                                                                    data-attachments-loaded="0">
+                                                                    <span class="my-1 text-primary c-pointer text-sm">
+                                                                        <i class="fa fa-paperclip"></i>
+                                                                        {{ count($message->attachments) }}
+                                                                        attachment{{ count($message->attachments) === 1 ? '' : 's' }}
+                                                                    </span>
+                                                                </div>
+                                                            @endif
+                                                        </div>
+                                                    </div>
+                                                </div>
+                                            @endforeach
+                                        </div>
+                                    </div>
+                                    @if ($toUser)
+                                        <div class="im-input">
+                                            <div class="d-flex align-items-end">
+                                                <div class="msg-input">
+                                                    <textarea placeholder="Enter your message here..."></textarea>
+                                                    <button class="btn btn-sm btn-input" id="im-btn-send"><i
+                                                            class="fa fa-paper-plane"></i></button>
+                                                </div>
+                                                <div class="fabs ml-2">
+                                                    <button class="btn btn-sm btn-info ml-2" id="im-btn-select-file"><i
+                                                            class="fa fa-paperclip"></i></button>
+                                                </div>
+                                            </div>
+                                            <div class="d-flex align-items-end flex-wrap" id="selected-files">
+                                            </div>
+                                        </div>
+                                    @endif
+
+                                    <div>
+                                        {{ $messages->withQueryString()->links() }}
+                                    </div>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+        <script>
+            $(document).ready(function() {
+
+                setTimeout(function() {
+                    jQuery('.mark-as-read').click();
+                }, 1000);
+
+                let inProgress = false;
+
+                function showSelectedFiles() {
+                    $('#selected-files').empty();
+                    $('.im-file-upload').each(function() {
+                        if (this.files && this.files.length) {
+                            for (let i = 0; i < this.files.length; i++) {
+                                $('#selected-files').append($('<div class="selected-file" data-id="' + this.id +
+                                    '" title="Click to remove">').text(this.files[i].name));
+                            }
+                        }
+                    });
+                }
+
+                function hasError(_data) {
+                    let msg = 'Unknown error!';
+                    if (_data) {
+                        if (_data.success) return false;
+                        else if (_data.message) msg = _data.message;
+                    }
+                    toastr.error(msg);
+                    return true;
+                }
+
+                function doSend(_elem) {
+                    if (inProgress) return false;
+                    inProgress = true;
+                    showMask();
+                    let text = $.trim(_elem.value);
+                    let formData = new FormData();
+                    formData.set('fromUserUid', '{{ $performer->user->uid }}');
+                    formData.set('toUserUid', '{{ isset($toUser) ? $toUser->uid : null }}');
+                    formData.set('contentText', text);
+
+                    let hasFiles = false;
+                    $('.im-file-upload').each(function() {
+                        if (this.files && this.files.length) {
+                            for (let i = 0; i < this.files.length; i++) {
+                                formData.append('attachments', this.files[i]);
+                                hasFiles = true;
+                            }
+                        }
+                    });
+
+                    if (!hasFiles && !text) { // either attachment or text or both should be there
+                        inProgress = false;
+                        hideMask();
+                        return false;
+                    }
+
+                    jQuery.ajax('/api/message/create', {
+                        dataType: 'json',
+                        data: formData,
+                        processData: false,
+                        contentType: false,
+                        type: 'POST',
+                    }).done(function(_data) {
+                        if (!hasError(_data)) {
+                            fastReload();
+                        } else {
+                            $('.im-input textarea').val('');
+                            inProgress = false;
+                            hideMask();
+                        }
+                    });
+                    return false;
+                }
+
+
+                function init() {
+                    @if (isset($user))
+                        $('.im-input textarea').on('keydown', function(_e) {
+                            if (_e.which === 13 && !_e.shiftKey) {
+                                return doSend(this);
+                            }
+                        });
+
+                        $('#im-btn-send').click(function() {
+                            return doSend($('.im-input textarea')[0]);
+                        });
+
+                        $('#plusBtn').click(function() {
+                            $('.hide').toggle();
+                            $('.show').toggle();
+                        });
+
+                        $('#im-btn-select-file').click(function() {
+                            let fiID = Math.floor(Math.random() * 10000);
+                            let fileInput = $('<input type="file" class="d-none im-file-upload" id="fu-' +
+                                fiID + '">');
+                            $('.im-input').append(fileInput)
+                            fileInput.click();
+                        });
+
+                        $(document).on('change', '.im-file-upload', function() {
+                            showSelectedFiles();
+                        });
+
+                        $(document)
+                            .off('click', '.selected-file')
+                            .on('click', '.selected-file', function() {
+                                $('#' + $(this).attr('data-id')).remove();
+                                showSelectedFiles();
+                            });
+
+                        $(document)
+                            .off('click.load-attachments', '.attachments-container[data-attachments-loaded="0"]>span')
+                            .on('click.load-attachments', '.attachments-container[data-attachments-loaded="0"]>span',
+                                function() {
+                                    let container = $(this).closest('.attachments-container');
+                                    if (inProgress) return false;
+                                    inProgress = true;
+                                    $.get('/admin/users/view/{{$user->uid}}/messages/' + container.attr(
+                                        'data-message-uid') + '/attachments', (_data) => {
+                                        container.html(_data).attr('data-attachments-loaded', 1);
+                                    }).then(function() {
+                                        inProgress = false;
+                                    });
+                                });
+
+                        $(document)
+                            .off('click', '.mark-as-read')
+                            .on('click', '.mark-as-read', function() {
+                                $.post('/api/message/markRead', {
+                                    uid: $(this).attr('data-message-uid')
+                                }, () => {
+                                    $(this).replaceWith(
+                                        '<i class="fa fa-check text-secondary on-hover-opaque text-sm"></i>'
+                                    );
+                                    let unreadBadge = $('.unread-badge[data-user-id="' +
+                                        {{ $user->id }} + '"]').first();
+                                    if (unreadBadge.length) {
+                                        let newCount = (+unreadBadge.text()) - 1;
+                                        if (newCount) {
+                                            unreadBadge.text(newCount);
+                                        } else {
+                                            unreadBadge.remove();
+                                        }
+                                    }
+                                }, 'json');
+                                return false;
+                            });
+
+                        $(document)
+                            .off('click', '.remove-message')
+                            .on('click', '.remove-message', function() {
+                                $.post('/api/message/markAsRemoved', {
+                                    uid: $(this).attr('data-message-uid')
+                                }, () => {
+                                    $(this).closest('.im-message').html(
+                                        '<div class="im-message-content text-secondary font-italic">This message was removed.</div>'
+                                    );
+                                }, 'json');
+                                return false;
+                            });
+
+                        var imScroller = document.getElementById("im-scroller");
+                        imScroller.scrollTop = imScroller.scrollHeight;
+                        $(imScroller).removeClass('opacity-0');
+                    @endif
+
+                }
+                init();
+            });
+        </script>
+    </div>
+
+@endsection

+ 1 - 0
resources/views/layouts/app-no-footer.blade.php

@@ -18,6 +18,7 @@
     <link rel="stylesheet" href="{{ asset('/css/yemi.css') }}" />
     <link rel="stylesheet" href="{{ asset('/css/fe2.css') }}" />
     <link rel="stylesheet" href="{{asset('/css/style.css')}}" />
+    <link rel="stylesheet" href="{{asset('/css/messages.css')}}" />
     <script src="https://code.jquery.com/jquery-3.6.1.min.js" integrity="sha256-o88AwQnZB+VDvE9tvIXrMQaPlFFSUTR+nldQm1LuPXQ=" crossorigin="anonymous"></script>
     <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery.mask/1.14.16/jquery.mask.min.js"></script>
     <script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.10.2/dist/umd/popper.min.js" integrity="sha384-7+zCNj/IqJ95wo16oMtfsKbZ9ccEh31eOz1HGyDuCQ6wgnyJNSYdrPa03rtR1zdB" crossorigin="anonymous"></script>

+ 2 - 0
routes/web.php

@@ -55,6 +55,8 @@ Route::group(['middleware' => ['ensureUserLoggedIn']], function () {
                 Route::get('/sessions', [AdminController::class, 'userSessions'])->name('.sessions');
                 Route::get('/user-events', [AdminController::class, 'userUserEvents'])->name('.user-events');
                 Route::get('/payment-methods', [AdminController::class, 'userPaymentMethods'])->name('.payment-methods');
+                Route::get('/messages', [AdminController::class, 'userMessages'])->name('.messages');
+                Route::get('messages/{message}/attachments', [AdminController::class, 'userMessageAttachments'])->name('message-attachments');
             });
             
             Route::get('/orders', [AdminController::class, 'orders'])->name('.orders');