Ver Fonte

Merge branch 'frontend-ui-refactor' into feat/double-tap-to-like

daniel há 6 anos atrás
pai
commit
7ceaa25205

+ 6 - 14
.env.example

@@ -1,11 +1,11 @@
-APP_NAME="PixelFed Test"
-APP_ENV=local
+APP_NAME="PixelFed Prod"
+APP_ENV=production
 APP_KEY=
-APP_DEBUG=true
-APP_URL=http://localhost
+APP_DEBUG=false
 
-ADMIN_DOMAIN="localhost"
+APP_URL=http://localhost
 APP_DOMAIN="localhost"
+ADMIN_DOMAIN="localhost"
 SESSION_DOMAIN="localhost"
 SESSION_SECURE_COOKIE=true
 TRUST_PROXIES="*"
@@ -38,22 +38,14 @@ MAIL_ENCRYPTION=null
 MAIL_FROM_ADDRESS="pixelfed@example.com"
 MAIL_FROM_NAME="Pixelfed"
 
-API_BASE="/api/1/"
-API_SEARCH="/api/search"
-
 OPEN_REGISTRATION=true
 ENFORCE_EMAIL_VERIFICATION=true
+PF_MAX_USERS=1000
 
 MAX_PHOTO_SIZE=15000
 MAX_CAPTION_LENGTH=150
 MAX_ALBUM_LENGTH=4
 
-MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
-MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
-MIX_APP_URL="${APP_URL}"
-MIX_API_BASE="${API_BASE}"
-MIX_API_SEARCH="${API_SEARCH}"
-
 ACTIVITY_PUB=false
 REMOTE_FOLLOW=false
 ACTIVITYPUB_INBOX=false

+ 21 - 17
.env.testing

@@ -2,10 +2,13 @@ APP_NAME="PixelFed Test"
 APP_ENV=local
 APP_KEY=base64:lwX95GbNWX3XsucdMe0XwtOKECta3h/B+p9NbH2jd0E=
 APP_DEBUG=true
-APP_URL=https://pixelfed.dev
 
-ADMIN_DOMAIN="pixelfed.dev"
+APP_URL=https://pixelfed.dev
 APP_DOMAIN="pixelfed.dev"
+ADMIN_DOMAIN="pixelfed.dev"
+SESSION_DOMAIN="pixelfed.dev"
+SESSION_SECURE_COOKIE=true
+TRUST_PROXIES="*"
 
 LOG_CHANNEL=stack
 
@@ -35,28 +38,29 @@ MAIL_ENCRYPTION=null
 MAIL_FROM_ADDRESS="pixelfed@example.com"
 MAIL_FROM_NAME="Pixelfed"
 
-SESSION_DOMAIN="${APP_DOMAIN}"
-SESSION_SECURE_COOKIE=true
-API_BASE="/api/1/"
-API_SEARCH="/api/search"
-
-OPEN_REGISTRATION=false
-ENFORCE_EMAIL_VERIFICATION=true
+OPEN_REGISTRATION=true
+ENFORCE_EMAIL_VERIFICATION=false
+PF_MAX_USERS=1000
 
 MAX_PHOTO_SIZE=15000
 MAX_CAPTION_LENGTH=150
 MAX_ALBUM_LENGTH=4
 
-MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
-MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
-MIX_APP_URL="${APP_URL}"
-MIX_API_BASE="${API_BASE}"
-MIX_API_SEARCH="${API_SEARCH}"
-
-TELESCOPE_ENABLED=false
-PF_MAX_USERS=1000
+ACTIVITY_PUB=false
+REMOTE_FOLLOW=false
+ACTIVITYPUB_INBOX=false
+ACTIVITYPUB_SHAREDINBOX=false
+# Set these "true" to enable federation.
+# You might need to also run:
+#   php artisan cache:clear
+#   php artisan optimize:clear
+#   php artisan optimize
 
 PF_COSTAR_ENABLED=true
 CS_BLOCKED_DOMAINS='example.org,example.net,example.com'
 CS_CW_DOMAINS='example.org,example.net,example.com'
 CS_UNLISTED_DOMAINS='example.org,example.net,example.com'
+
+## Optional 
+#HORIZON_DARKMODE=false  # Horizon theme darkmode
+#HORIZON_EMBED=false  # Single Docker Container mode 

+ 25 - 1
app/Http/Controllers/Settings/LabsSettings.php

@@ -24,7 +24,8 @@ trait LabsSettings {
 		$this->validate($request, [
 			'profile_layout' => 'nullable',
 			'dark_mode'	=> 'nullable',
-			'profile_suggestions' => 'nullable'
+			'profile_suggestions' => 'nullable',
+			'moment_bg' => 'nullable'
 		]);
 
 		$changes = false;
@@ -60,6 +61,12 @@ trait LabsSettings {
 			SuggestionService::del($profile->id);
 		}
 
+		if($request->has('moment_bg') && $profile->profile_layout == 'moment') {
+			$bg = in_array($request->input('moment_bg'), $this->momentBackgrounds()) ? $request->input('moment_bg') : 'default';
+			$profile->header_bg = $bg;
+			$changes = true;
+		}
+
 		if($changes == true) {
 			$profile->save();
 		}
@@ -69,4 +76,21 @@ trait LabsSettings {
 			->cookie($cookie);
 	}
 
+	protected function momentBackgrounds()
+	{
+		return [
+			'default',
+			'azure',
+			'passion',
+			'reef',
+			'lush',
+			'neon',
+			'flare',
+			'morning',
+			'tranquil',
+			'mauve',
+			'argon',
+			'royal'
+		];
+	}
 }

+ 2 - 8
app/Jobs/LikePipeline/LikePipeline.php

@@ -69,19 +69,13 @@ class LikePipeline implements ShouldQueue
             $notification->profile_id = $status->profile_id;
             $notification->actor_id = $actor->id;
             $notification->action = 'like';
-            $notification->message = $like->toText();
-            $notification->rendered = $like->toHtml();
+            $notification->message = $like->toText($status->in_reply_to_id ? 'comment' : 'post');
+            $notification->rendered = $like->toHtml($status->in_reply_to_id ? 'comment' : 'post');
             $notification->item_id = $status->id;
             $notification->item_type = "App\Status";
             $notification->save();
 
-            Cache::forever('notification.'.$notification->id, $notification);
-
-            $redis = Redis::connection();
-            $key = config('cache.prefix').':user.'.$status->profile_id.'.notifications';
-            $redis->lpush($key, $notification->id);
         } catch (Exception $e) {
-            Log::error($e);
         }
     }
 }

+ 6 - 5
app/Like.php

@@ -27,19 +27,20 @@ class Like extends Model
         return $this->belongsTo(Status::class);
     }
 
-    public function toText()
+    public function toText($type = 'post')
     {
         $actorName = $this->actor->username;
+        $msg = $type == 'post' ? __('notification.likedPhoto') : __('notification.likedComment');
 
-        return "{$actorName} ".__('notification.likedPhoto');
+        return "{$actorName} ".$msg;
     }
 
-    public function toHtml()
+    public function toHtml($type = 'post')
     {
         $actorName = $this->actor->username;
         $actorUrl = $this->actor->url();
+        $msg = $type == 'post' ? __('notification.likedPhoto') : __('notification.likedComment');
 
-        return "<a href='{$actorUrl}' class='profile-link'>{$actorName}</a> ".
-          __('notification.likedPhoto');
+        return "<a href='{$actorUrl}' class='profile-link'>{$actorName}</a> ".$msg;
     }
 }

+ 4 - 11
app/Providers/HorizonServiceProvider.php

@@ -20,6 +20,10 @@ class HorizonServiceProvider extends HorizonApplicationServiceProvider
         // Horizon::routeSmsNotificationsTo('15556667777');
         // Horizon::routeMailNotificationsTo('example@example.com');
         // Horizon::routeSlackNotificationsTo('slack-webhook-url', '#channel');
+        
+        if(config('horizon.darkmode') == true) {
+            Horizon::night();
+        }
     }
 
     /**
@@ -36,15 +40,4 @@ class HorizonServiceProvider extends HorizonApplicationServiceProvider
         });
     }
 
-    /**
-     * Register any application services.
-     *
-     * @return void
-     */
-    public function register()
-    {
-        if(config('horizon.darkmode') == true) {
-            Horizon::night();
-        }
-    }
 }

+ 1 - 0
app/Transformer/Api/AccountTransformer.php

@@ -26,6 +26,7 @@ class AccountTransformer extends Fractal\TransformerAbstract
 			'avatar_static' => $profile->avatarUrl(),
 			'header' => null,
 			'header_static' => null,
+			'header_bg' => $profile->header_bg,
 			'moved' => null,
 			'fields' => null,
 			'bot' => null,

+ 1 - 1
composer.json

@@ -18,7 +18,7 @@
         "intervention/image": "^2.4",
         "jenssegers/agent": "^2.6",
         "laravel/framework": "5.8.*",
-        "laravel/horizon": "^3.0",
+        "laravel/horizon": "^3.1",
         "laravel/passport": "^7.0",
         "laravel/tinker": "^1.0",
         "league/flysystem-aws-s3-v3": "~1.0",

+ 22 - 22
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": "e7370fab05135d2b5e1161ccfc821f17",
+    "content-hash": "36dce3c2a72bd07cacbd5e9f38e568f4",
     "packages": [
         {
             "name": "alchemy/binary-driver",
@@ -71,16 +71,16 @@
         },
         {
             "name": "aws/aws-sdk-php",
-            "version": "3.93.1",
+            "version": "3.93.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/aws/aws-sdk-php.git",
-                "reference": "2dce6e4b7295c6ea44392fc8eff421e3651a8725"
+                "reference": "874c1040edab52df3873157aa54ea51833d48c0e"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/2dce6e4b7295c6ea44392fc8eff421e3651a8725",
-                "reference": "2dce6e4b7295c6ea44392fc8eff421e3651a8725",
+                "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/874c1040edab52df3873157aa54ea51833d48c0e",
+                "reference": "874c1040edab52df3873157aa54ea51833d48c0e",
                 "shasum": ""
             },
             "require": {
@@ -150,7 +150,7 @@
                 "s3",
                 "sdk"
             ],
-            "time": "2019-05-01T18:10:22+00:00"
+            "time": "2019-05-03T18:07:06+00:00"
         },
         {
             "name": "beyondcode/laravel-self-diagnosis",
@@ -2262,16 +2262,16 @@
         },
         {
             "name": "league/oauth2-server",
-            "version": "7.3.3",
+            "version": "7.4.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/thephpleague/oauth2-server.git",
-                "reference": "c7f499849704ebe2c60b45b6d6bb231df5601d4a"
+                "reference": "2eb1cf79e59d807d89c256e7ac5e2bf8bdbd4acf"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/thephpleague/oauth2-server/zipball/c7f499849704ebe2c60b45b6d6bb231df5601d4a",
-                "reference": "c7f499849704ebe2c60b45b6d6bb231df5601d4a",
+                "url": "https://api.github.com/repos/thephpleague/oauth2-server/zipball/2eb1cf79e59d807d89c256e7ac5e2bf8bdbd4acf",
+                "reference": "2eb1cf79e59d807d89c256e7ac5e2bf8bdbd4acf",
                 "shasum": ""
             },
             "require": {
@@ -2335,7 +2335,7 @@
                 "secure",
                 "server"
             ],
-            "time": "2019-03-29T18:19:35+00:00"
+            "time": "2019-05-05T09:22:01+00:00"
         },
         {
             "name": "mobiledetect/mobiledetectlib",
@@ -2724,16 +2724,16 @@
         },
         {
             "name": "opis/closure",
-            "version": "3.1.6",
+            "version": "3.2.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/opis/closure.git",
-                "reference": "ccb8e3928c5c8181c76cdd0ed9366c5bcaafd91b"
+                "reference": "09b4389715a7eec100176ea58286649181753508"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/opis/closure/zipball/ccb8e3928c5c8181c76cdd0ed9366c5bcaafd91b",
-                "reference": "ccb8e3928c5c8181c76cdd0ed9366c5bcaafd91b",
+                "url": "https://api.github.com/repos/opis/closure/zipball/09b4389715a7eec100176ea58286649181753508",
+                "reference": "09b4389715a7eec100176ea58286649181753508",
                 "shasum": ""
             },
             "require": {
@@ -2746,7 +2746,7 @@
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "3.1.x-dev"
+                    "dev-master": "3.2.x-dev"
                 }
             },
             "autoload": {
@@ -2781,7 +2781,7 @@
                 "serialization",
                 "serialize"
             ],
-            "time": "2019-02-22T10:30:00+00:00"
+            "time": "2019-05-05T12:50:25+00:00"
         },
         {
             "name": "paragonie/constant_time_encoding",
@@ -7168,16 +7168,16 @@
         },
         {
             "name": "sebastian/environment",
-            "version": "4.2.1",
+            "version": "4.2.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/environment.git",
-                "reference": "3095910f0f0fb155ac4021fc51a4a7a39ac04e8a"
+                "reference": "f2a2c8e1c97c11ace607a7a667d73d47c19fe404"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/3095910f0f0fb155ac4021fc51a4a7a39ac04e8a",
-                "reference": "3095910f0f0fb155ac4021fc51a4a7a39ac04e8a",
+                "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/f2a2c8e1c97c11ace607a7a667d73d47c19fe404",
+                "reference": "f2a2c8e1c97c11ace607a7a667d73d47c19fe404",
                 "shasum": ""
             },
             "require": {
@@ -7217,7 +7217,7 @@
                 "environment",
                 "hhvm"
             ],
-            "time": "2019-04-25T07:55:20+00:00"
+            "time": "2019-05-05T09:05:15+00:00"
         },
         {
             "name": "sebastian/exporter",

+ 1 - 0
config/app.php

@@ -158,6 +158,7 @@ return [
         App\Providers\AppServiceProvider::class,
         App\Providers\AuthServiceProvider::class,
         // App\Providers\BroadcastServiceProvider::class,
+        App\Providers\HorizonServiceProvider::class,
         App\Providers\EventServiceProvider::class,
         App\Providers\RouteServiceProvider::class,
 

+ 67 - 0
config/horizon.php

@@ -2,6 +2,32 @@
 
 return [
 
+    /*
+    |--------------------------------------------------------------------------
+    | Horizon Domain
+    |--------------------------------------------------------------------------
+    |
+    | This is the subdomain where Horizon will be accessible from. If this
+    | setting is null, Horizon will reside under the same domain as the
+    | application. Otherwise, this value will serve as the subdomain.
+    |
+    */
+
+    'domain' => null,
+
+    /*
+    |--------------------------------------------------------------------------
+    | Horizon Path
+    |--------------------------------------------------------------------------
+    |
+    | This is the URI path where Horizon will be accessible from. Feel free
+    | to change this path to anything you like. Note that the URI will not
+    | affect the paths of its internal API that aren't exposed to users.
+    |
+    */
+
+    'path' => 'horizon',
+
     /*
     |--------------------------------------------------------------------------
     | Horizon Redis Connection
@@ -28,6 +54,19 @@ return [
 
     'prefix' => env('HORIZON_PREFIX', 'horizon-'.str_random(8).':'),
 
+    /*
+    |--------------------------------------------------------------------------
+    | Horizon Route Middleware
+    |--------------------------------------------------------------------------
+    |
+    | These middleware will get attached onto each Horizon route, giving you
+    | the chance to add your own middleware to this list or change any of
+    | the existing middleware. Or, you can simply stick with this list.
+    |
+    */
+
+    'middleware' => ['web'],
+
     /*
     |--------------------------------------------------------------------------
     | Queue Wait Time Thresholds
@@ -61,6 +100,34 @@ return [
         'failed' => 10080,
     ],
 
+    /*
+    |--------------------------------------------------------------------------
+    | Fast Termination
+    |--------------------------------------------------------------------------
+    |
+    | When this option is enabled, Horizon's "terminate" command will not
+    | wait on all of the workers to terminate unless the --wait option
+    | is provided. Fast termination can shorten deployment delay by
+    | allowing a new instance of Horizon to start while the last
+    | instance will continue to terminate each of its workers.
+    |
+    */
+
+    'fast_termination' => false,
+
+    /*
+    |--------------------------------------------------------------------------
+    | Memory Limit (MB)
+    |--------------------------------------------------------------------------
+    |
+    | This value describes the maximum amount of memory the Horizon worker
+    | may consume before it is terminated and restarted. You should set
+    | this value according to the resources available to your server.
+    |
+    */
+
+    'memory_limit' => 64,
+
     /*
     |--------------------------------------------------------------------------
     | Queue Worker Configuration

+ 32 - 0
database/migrations/2019_05_04_174911_add_header_to_profiles_table.php

@@ -0,0 +1,32 @@
+<?php
+
+use Illuminate\Support\Facades\Schema;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Database\Migrations\Migration;
+
+class AddHeaderToProfilesTable extends Migration
+{
+    /**
+     * Run the migrations.
+     *
+     * @return void
+     */
+    public function up()
+    {
+        Schema::table('profiles', function (Blueprint $table) {
+            $table->string('header_bg')->nullable()->after('profile_layout');
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     *
+     * @return void
+     */
+    public function down()
+    {
+        Schema::table('profiles', function (Blueprint $table) {
+            $table->dropColumn('header_bg');
+        });
+    }
+}

BIN
public/css/app.css


BIN
public/css/appdark.css


BIN
public/js/compose.js


BIN
public/js/profile.js


BIN
public/js/status.js


BIN
public/js/timeline.js


BIN
public/mix-manifest.json


+ 3 - 3
resources/assets/js/components/ComposeModal.vue

@@ -34,10 +34,10 @@
 						</div>
 					</div>
 					<div v-else>
-						<div v-if="ids.length > 0 && ids.length != config.uploader.album_limit" class="card-header py-2 bg-primary m-2 rounded cursor-pointer" v-on:click="addMedia()">
+						<div v-if="ids.length > 0 && ids.length != config.uploader.album_limit" class="card-header py-2 bg-primary m-2 rounded cursor-pointer" v-on:click="addMedia($event)">
 							<p class="text-center mb-0 font-weight-bold text-white"><i class="fas fa-plus mr-1"></i> Add Photo</p>
 						</div>
-						<div v-if="ids.length == 0" class="w-100 h-100 bg-light py-5 cursor-pointer" style="border-bottom: 1px solid #f1f1f1" v-on:click="addMedia()">
+						<div v-if="ids.length == 0" class="w-100 h-100 bg-light py-5 cursor-pointer" style="border-bottom: 1px solid #f1f1f1" v-on:click="addMedia($event)">
 							<p class="text-center mb-0 font-weight-bold p-5">Click here to add photos</p>
 						</div>
 						<div v-if="ids.length > 0">
@@ -316,7 +316,7 @@ export default {
 			});
 		},
 
-		addMedia() {
+		addMedia(event) {
 			let el = $(event.target);
 			el.attr('disabled', '');
 			let fi = $('.file-input[name="media"]');

+ 31 - 1
resources/assets/js/components/NotificationCard.vue

@@ -67,7 +67,8 @@
 		data() {
 			return {
 				notifications: {},
-				notificationCursor: 2
+				notificationCursor: 2,
+				notificationMaxId: 0,
 			};
 		},
 
@@ -91,9 +92,12 @@
 						}
 						return true;
 					});
+					let ids = res.data.map(n => n.id);
+					this.notificationMaxId = Math.max(...ids);
 					this.notifications = data;
 					$('.notification-card .loader').addClass('d-none');
 					$('.notification-card .contents').removeClass('d-none');
+					this.notificationPoll();
 				});
 			},
 
@@ -161,6 +165,32 @@
 				let username = status.account.username;
 				let id = status.id;
 				return '/p/' + username + '/' + id;
+			},
+
+			notificationPoll() {
+				let interval = this.notifications.length > 5 ? 15000 : 120000;
+				let self = this;
+				setInterval(function() {
+					axios.get('/api/v1/notifications')
+					.then(res => {
+						let data = res.data.filter(n => {
+							if(n.type == 'share' || self.notificationMaxId >= n.id) {
+								return false;
+							}
+							return true;
+						});
+						if(data.length) {
+							let ids = data.map(n => n.id);
+							self.notificationMaxId = Math.max(...ids);
+
+							self.notifications.unshift(...data);
+							let beep = new Audio('/static/beep.mp3');
+							beep.volume = 0.7;
+							beep.play();
+							$('.notification-card .far.fa-bell').addClass('fas text-danger').removeClass('far text-muted');
+						}
+					});
+				}, interval);
 			}
 		}
 	}

+ 2 - 2
resources/assets/js/components/PostComponent.vue

@@ -893,11 +893,11 @@ export default {
         let em = event.target.innerText;
         if(this.replyText.length == 0) {
           this.reply_to_profile_id = this.status.account.id;
-          this.replyText = '@' + this.status.account.username + ' ' + em;
+          this.replyText = em + ' ';
           $('textarea[name="comment"]').focus();
         } else {
           this.reply_to_profile_id = this.status.account.id;
-          this.replyText += em;
+          this.replyText += em + ' ';
           $('textarea[name="comment"]').focus();
         }
       },

+ 11 - 1
resources/assets/js/components/Profile.vue

@@ -294,7 +294,7 @@
 		</div>
 
 		<div v-if="profileLayout == 'moment'">
-			<div class="w-100 h-100 mt-n3 bg-pixelfed" style="width:100%;min-height:274px;">
+			<div :class="momentBackground()" style="width:100%;min-height:274px;">
 			</div>
 			<div class="bg-white border-bottom">
 				<div class="container">
@@ -1045,6 +1045,16 @@ export default {
 					this.profile.following_count--;
 				}
 			})
+		},
+
+		momentBackground() {
+			let c = 'w-100 h-100 mt-n3 ';
+			if(this.profile.header_bg) {
+				c += this.profile.header_bg == 'default' ? 'bg-pixelfed' : 'bg-moment-' + this.profile.header_bg;
+			} else {
+				c += 'bg-pixelfed';
+			}
+			return c;
 		}
 	}
 }

+ 216 - 117
resources/assets/js/components/Timeline.vue

@@ -8,125 +8,173 @@
 						<span class="sr-only">Loading...</span>
 					</div>
 				</div>
-				<div class="card mb-sm-4 status-card card-md-rounded-0" :data-status-id="status.id" v-for="(status, index) in feed" :key="`${index}-${status.id}`">
-
-					<div class="card-header d-inline-flex align-items-center bg-white">
-						<img v-bind:src="status.account.avatar" width="32px" height="32px" style="border-radius: 32px;">
-						<a class="username font-weight-bold pl-2 text-dark" v-bind:href="status.account.url">
-							{{status.account.username}}
-						</a>
-						<div class="text-right" style="flex-grow:1;">
-							<button class="btn btn-link text-dark no-caret dropdown-toggle py-0" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" title="Post options">
-								<span class="fas fa-ellipsis-v fa-lg text-muted"></span>
-							</button>
-							<div class="dropdown-menu dropdown-menu-right">
-								<a class="dropdown-item font-weight-bold" :href="status.url">Go to post</a>
-								<!-- <a class="dropdown-item font-weight-bold" href="#">Share</a>
-								<a class="dropdown-item font-weight-bold" href="#">Embed</a> -->
-								<span v-if="statusOwner(status) == false">
-									<a class="dropdown-item font-weight-bold" :href="reportUrl(status)">Report</a>
-									<a class="dropdown-item font-weight-bold" v-on:click="muteProfile(status)">Mute Profile</a>
-									<a class="dropdown-item font-weight-bold" v-on:click="blockProfile(status)">Block Profile</a>
-								</span>
-								<span v-if="statusOwner(status) == true">
-									<a class="dropdown-item font-weight-bold text-danger" v-on:click="deletePost(status)">Delete</a>
-								</span>
-								<span v-if="profile.is_admin == true && modes.mod == true">
-									<div class="dropdown-divider"></div>
-									<a v-if="!statusOwner(status)" class="dropdown-item font-weight-bold text-danger" v-on:click="deletePost(status)">Delete</a>
-									<div class="dropdown-divider"></div>
-									<h6 class="dropdown-header">Mod Tools</h6>
-									<a class="dropdown-item font-weight-bold" v-on:click="moderatePost(status, 'autocw')">
-										<p class="mb-0" data-toggle="tooltip" data-placement="bottom" title="Adds a CW to every post made by this account.">Enforce CW</p>
-									</a>
-									<a class="dropdown-item font-weight-bold" v-on:click="moderatePost(status, 'noautolink')">
-										<p class="mb-0" title="Do not transform mentions, hashtags or urls into HTML.">No Autolinking</p>
-									</a>
-									<a class="dropdown-item font-weight-bold" v-on:click="moderatePost(status, 'unlisted')">
-										<p class="mb-0" title="Removes account from public/network timelines.">Unlisted Posts</p>
-									</a>
-									<a class="dropdown-item font-weight-bold" v-on:click="moderatePost(status, 'disable')">
-										<p class="mb-0" title="Temporarily disable account until next time user log in.">Disable Account</p>
-									</a>
-									<a class="dropdown-item font-weight-bold" v-on:click="moderatePost(status, 'suspend')">
-										<p class="mb-0" title="This prevents any new interactions, without deleting existing data.">Suspend Account</p>
-									</a>
-
-								</span>
+				<div :data-status-id="status.id" v-for="(status, index) in feed" :key="`${index}-${status.id}`">
+					<div v-if="index == 2 && showSuggestions == true && suggestions.length" class="card mb-sm-4 status-card card-md-rounded-0">
+						<div class="card-header d-flex align-items-center justify-content-between bg-white border-0 pb-0">
+							<h6 class="text-muted font-weight-bold mb-0">Suggestions For You</h6>
+							<span class="cursor-pointer text-muted" v-on:click="hideSuggestions"><i class="fas fa-times"></i></span>
+						</div>
+						<div class="card-body row mx-0">
+							<div class="col-12 col-md-4 mb-3" v-for="(rec, index) in suggestions">
+								<div class="card">
+									<div class="card-body text-center pt-3">
+										<p class="mb-0">
+											<a :href="'/'+rec.username">
+												<img :src="rec.avatar" class="img-fluid rounded-circle cursor-pointer" width="45px" height="45px">
+											</a>
+										</p>
+										<div class="py-3">
+											<p class="font-weight-bold text-dark cursor-pointer mb-0">
+												<a :href="'/'+rec.username" class="text-decoration-none text-dark">
+													{{rec.username}}
+												</a>
+											</p>
+											<p class="small text-muted mb-0">{{rec.message}}</p>
+										</div>
+										<p class="mb-0">
+											<a class="btn btn-primary btn-block font-weight-bold py-0" href="#" @click.prevent="expRecFollow(rec.id, index)">Follow</a>
+										</p>
+									</div>
+								</div>
 							</div>
 						</div>
 					</div>
+					<div class="card mb-sm-4 status-card card-md-rounded-0">
+						<div class="card-header d-inline-flex align-items-center bg-white">
+							<img v-bind:src="status.account.avatar" width="32px" height="32px" style="border-radius: 32px;">
+							<a class="username font-weight-bold pl-2 text-dark" v-bind:href="status.account.url">
+								{{status.account.username}}
+							</a>
+							<div class="text-right" style="flex-grow:1;">
+								<button class="btn btn-link text-dark no-caret dropdown-toggle py-0" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" title="Post options">
+									<span class="fas fa-ellipsis-v fa-lg text-muted"></span>
+								</button>
+								<div class="dropdown-menu dropdown-menu-right">
+									<a class="dropdown-item font-weight-bold" :href="status.url">Go to post</a>
+									<!-- <a class="dropdown-item font-weight-bold" href="#">Share</a>
+									<a class="dropdown-item font-weight-bold" href="#">Embed</a> -->
+									<span v-if="statusOwner(status) == false">
+										<a class="dropdown-item font-weight-bold" :href="reportUrl(status)">Report</a>
+										<a class="dropdown-item font-weight-bold" v-on:click="muteProfile(status)">Mute Profile</a>
+										<a class="dropdown-item font-weight-bold" v-on:click="blockProfile(status)">Block Profile</a>
+									</span>
+									<span v-if="statusOwner(status) == true">
+										<a class="dropdown-item font-weight-bold text-danger" v-on:click="deletePost(status)">Delete</a>
+									</span>
+									<span v-if="profile.is_admin == true && modes.mod == true">
+										<div class="dropdown-divider"></div>
+										<a v-if="!statusOwner(status)" class="dropdown-item font-weight-bold text-danger" v-on:click="deletePost(status)">Delete</a>
+										<div class="dropdown-divider"></div>
+										<h6 class="dropdown-header">Mod Tools</h6>
+										<a class="dropdown-item font-weight-bold" v-on:click="moderatePost(status, 'autocw')">
+											<p class="mb-0" data-toggle="tooltip" data-placement="bottom" title="Adds a CW to every post made by this account.">Enforce CW</p>
+										</a>
+										<a class="dropdown-item font-weight-bold" v-on:click="moderatePost(status, 'noautolink')">
+											<p class="mb-0" title="Do not transform mentions, hashtags or urls into HTML.">No Autolinking</p>
+										</a>
+										<a class="dropdown-item font-weight-bold" v-on:click="moderatePost(status, 'unlisted')">
+											<p class="mb-0" title="Removes account from public/network timelines.">Unlisted Posts</p>
+										</a>
+										<a class="dropdown-item font-weight-bold" v-on:click="moderatePost(status, 'disable')">
+											<p class="mb-0" title="Temporarily disable account until next time user log in.">Disable Account</p>
+										</a>
+										<a class="dropdown-item font-weight-bold" v-on:click="moderatePost(status, 'suspend')">
+											<p class="mb-0" title="This prevents any new interactions, without deleting existing data.">Suspend Account</p>
+										</a>
 
-					<div class="postPresenterContainer" v-on:doubletap="likeStatus(status, $event)">
-						<div v-if="status.pf_type === 'photo'" class="w-100">
-							<photo-presenter :status="status" v-on:lightbox="lightbox"></photo-presenter>
+									</span>
+								</div>
+							</div>
 						</div>
 
-						<div v-else-if="status.pf_type === 'video'" class="w-100">
-							<video-presenter :status="status"></video-presenter>
-						</div>
+						<div class="postPresenterContainer" v-on:doubletap="likeStatus(status, $event)">
+							<div v-if="status.pf_type === 'photo'" class="w-100">
+								<photo-presenter :status="status" v-on:lightbox="lightbox"></photo-presenter>
+							</div>
 
-						<div v-else-if="status.pf_type === 'photo:album'" class="w-100">
-							<photo-album-presenter :status="status" v-on:lightbox="lightbox"></photo-album-presenter>
-						</div>
+							<div v-else-if="status.pf_type === 'video'" class="w-100">
+								<video-presenter :status="status"></video-presenter>
+							</div>
 
-						<div v-else-if="status.pf_type === 'video:album'" class="w-100">
-							<video-album-presenter :status="status"></video-album-presenter>
-						</div>
+							<div v-else-if="status.pf_type === 'photo:album'" class="w-100">
+								<photo-album-presenter :status="status" v-on:lightbox="lightbox"></photo-album-presenter>
+							</div>
 
-						<div v-else-if="status.pf_type === 'photo:video:album'" class="w-100">
-							<mixed-album-presenter :status="status" v-on:lightbox="lightbox"></mixed-album-presenter>
-						</div>
+							<div v-else-if="status.pf_type === 'video:album'" class="w-100">
+								<video-album-presenter :status="status"></video-album-presenter>
+							</div>
 
-						<div v-else class="w-100">
-							<p class="text-center p-0 font-weight-bold text-white">Error: Problem rendering preview.</p>
-						</div>
-					</div>
+							<div v-else-if="status.pf_type === 'photo:video:album'" class="w-100">
+								<mixed-album-presenter :status="status" v-on:lightbox="lightbox"></mixed-album-presenter>
+							</div>
 
-					<div class="card-body">
-						<div class="reactions my-1">
-							<h3 v-bind:class="[status.favourited ? 'fas fa-heart text-danger pr-3 m-0 cursor-pointer' : 'far fa-heart pr-3 m-0 like-btn cursor-pointer']" title="Like" v-on:click="likeStatus(status, $event)"></h3>
-							<h3 v-if="!status.comments_disabled" class="far fa-comment pr-3 m-0 cursor-pointer" title="Comment" v-on:click="commentFocus(status, $event)"></h3>
-							<h3 v-bind:class="[status.reblogged ? 'far fa-share-square pr-3 m-0 text-primary cursor-pointer' : 'far fa-share-square pr-3 m-0 share-btn cursor-pointer']" title="Share" v-on:click="shareStatus(status, $event)"></h3>
+							<div v-else class="w-100">
+								<p class="text-center p-0 font-weight-bold text-white">Error: Problem rendering preview.</p>
+							</div>
 						</div>
 
-						<div class="likes font-weight-bold" v-if="expLc(status) == true">
-							<span class="like-count">{{status.favourites_count}}</span> {{status.favourites_count == 1 ? 'like' : 'likes'}}
-						</div>
-						<div class="caption">
-							<p class="mb-2 read-more" style="overflow: hidden;">
-								<span class="username font-weight-bold">
-									<bdi><a class="text-dark" :href="status.account.url">{{status.account.username}}</a></bdi>
-								</span>
-								<span v-html="status.content"></span>
-							</p>
-						</div>
-						<div class="comments" v-if="status.id == replyId && !status.comments_disabled">
-							<p class="mb-0 d-flex justify-content-between align-items-top read-more" style="overflow-y: hidden;" v-for="(reply, index) in replies">
-								<span>
-									<a class="text-dark font-weight-bold mr-1" :href="reply.account.url">{{reply.account.username}}</a>
-									<span v-html="reply.content"></span>
-								</span>
-								<span class="mb-0" style="min-width:38px">
-									<span v-on:click="likeStatus(reply, $event)"><i v-bind:class="[reply.favourited ? 'fas fa-heart fa-sm text-danger':'far fa-heart fa-sm text-lighter']"></i></span>
-									<post-menu :status="reply" :profile="profile" size="sm" :modal="'true'" :feed="feed" class="d-inline-flex pl-2"></post-menu>
-								</span>
-							</p>
+						<div class="card-body">
+							<div class="reactions my-1">
+								<h3 v-bind:class="[status.favourited ? 'fas fa-heart text-danger pr-3 m-0 cursor-pointer' : 'far fa-heart pr-3 m-0 like-btn cursor-pointer']" title="Like" v-on:click="likeStatus(status, $event)"></h3>
+								<h3 v-if="!status.comments_disabled" class="far fa-comment pr-3 m-0 cursor-pointer" title="Comment" v-on:click="commentFocus(status, $event)"></h3>
+								<h3 v-bind:class="[status.reblogged ? 'far fa-share-square pr-3 m-0 text-primary cursor-pointer' : 'far fa-share-square pr-3 m-0 share-btn cursor-pointer']" title="Share" v-on:click="shareStatus(status, $event)"></h3>
+							</div>
+
+							<div class="likes font-weight-bold" v-if="expLc(status) == true">
+								<span class="like-count">{{status.favourites_count}}</span> {{status.favourites_count == 1 ? 'like' : 'likes'}}
+							</div>
+							<div class="caption">
+								<p class="mb-2 read-more" style="overflow: hidden;">
+									<span class="username font-weight-bold">
+										<bdi><a class="text-dark" :href="status.account.url">{{status.account.username}}</a></bdi>
+									</span>
+									<span v-html="status.content"></span>
+								</p>
+							</div>
+							<div class="comments" v-if="status.id == replyId && !status.comments_disabled">
+								<p class="mb-0 d-flex justify-content-between align-items-top read-more" style="overflow-y: hidden;" v-for="(reply, index) in replies">
+									<span>
+										<a class="text-dark font-weight-bold mr-1" :href="reply.account.url">{{reply.account.username}}</a>
+										<span v-html="reply.content"></span>
+									</span>
+									<span class="mb-0" style="min-width:38px">
+										<span v-on:click="likeStatus(reply, $event)"><i v-bind:class="[reply.favourited ? 'fas fa-heart fa-sm text-danger':'far fa-heart fa-sm text-lighter']"></i></span>
+										<post-menu :status="reply" :profile="profile" size="sm" :modal="'true'" :feed="feed" class="d-inline-flex pl-2"></post-menu>
+									</span>
+								</p>
+							</div>
+							<div class="timestamp mt-2">
+								<p class="small text-uppercase mb-0">
+									<a :href="status.url" class="text-muted">
+										<timeago :datetime="status.created_at" :auto-update="60" :converter-options="{includeSeconds:true}" :title="timestampFormat(status.created_at)" v-b-tooltip.hover.bottom></timeago>
+									</a>
+								</p>
+							</div>
 						</div>
-						<div class="timestamp mt-2">
-							<p class="small text-uppercase mb-0">
-								<a :href="status.url" class="text-muted">
-									<timeago :datetime="status.created_at" :auto-update="60" :converter-options="{includeSeconds:true}" :title="timestampFormat(status.created_at)" v-b-tooltip.hover.bottom></timeago>
-								</a>
-							</p>
+
+						<div v-if="status.id == replyId && !status.comments_disabled" class="card-footer bg-white px-2 py-0">
+							<ul class="nav align-items-center emoji-reactions" style="overflow-x: scroll;flex-wrap: unset;">
+								<li class="nav-item" v-on:click="emojiReaction(status)">😂</li>
+								<li class="nav-item" v-on:click="emojiReaction(status)">💯</li>
+								<li class="nav-item" v-on:click="emojiReaction(status)">❤️</li>
+								<li class="nav-item" v-on:click="emojiReaction(status)">🙌</li>
+								<li class="nav-item" v-on:click="emojiReaction(status)">👏</li>
+								<li class="nav-item" v-on:click="emojiReaction(status)">😍</li>
+								<li class="nav-item" v-on:click="emojiReaction(status)">😯</li>
+								<li class="nav-item" v-on:click="emojiReaction(status)">😢</li>
+								<li class="nav-item" v-on:click="emojiReaction(status)">😅</li>
+								<li class="nav-item" v-on:click="emojiReaction(status)">😁</li>
+								<li class="nav-item" v-on:click="emojiReaction(status)">🙂</li>
+								<li class="nav-item" v-on:click="emojiReaction(status)">😎</li>
+							</ul>
 						</div>
-					</div>
 
-					<div class="card-footer bg-white" v-if="status.id == replyId">
-						<form class="" v-on:submit.prevent="commentSubmit(status, $event)">
-							<input type="hidden" name="item" value="">
-							<input class="form-control status-reply-input" name="comment" placeholder="Add a comment…" autocomplete="off">
-						</form>
+						<div v-if="status.id == replyId && !status.comments_disabled" class="card-footer bg-white sticky-md-bottom p-0">
+							<form class="border-0 rounded-0 align-middle" method="post" action="/i/comment" :data-id="status.id" data-truncate="false">
+								<textarea class="form-control border-0 rounded-0" name="comment" placeholder="Add a comment…" autocomplete="off" autocorrect="off" style="height:56px;line-height: 18px;max-height:80px;resize: none; padding-right:4.2rem;" v-model="replyText"></textarea>
+								<input type="button" value="Post" class="d-inline-block btn btn-link font-weight-bold reply-btn text-decoration-none" v-on:click.prevent="commentSubmit(status, $event)"/>
+							</form>
+						</div>
 					</div>
 				</div>
 				<div v-if="modes.infinite == true && !loading && feed.length > 0">
@@ -220,10 +268,12 @@
 					<notification-card></notification-card>
 				</div>
 
-				<div v-show="suggestions.length && config.ab && config.ab.rec == true" class="mb-4">
+				<div v-show="showSuggestions == true && suggestions.length && config.ab && config.ab.rec == true" class="mb-4">
 					<div class="card">
-						<div class="card-header bg-white text-center">
+						<div class="card-header bg-white d-flex align-items-center justify-content-between">
+							<div></div>
 							<div class="small text-dark text-uppercase font-weight-bold">Suggestions</div>
+							<div class="small text-muted cursor-pointer" v-on:click="hideSuggestions"><i class="fas fa-times"></i></div>
 						</div>
 						<div class="card-body pt-0">
 							<div v-for="(rec, index) in suggestions" class="media align-items-center mt-3">
@@ -355,6 +405,24 @@
 	.small .custom-control-label {
 		padding-top: 3px;
 	}
+	.reply-btn {
+		position: absolute;
+		bottom: 12px;
+		right: 20px;
+		width: 60px;
+		text-align: center;
+		border-radius: 0 3px 3px 0;
+	}
+	.emoji-reactions .nav-item {
+		font-size: 1.2rem;
+		padding: 9px;
+		cursor: pointer;
+	}
+	.emoji-reactions::-webkit-scrollbar {
+		width: 0px;
+		height: 0px;
+		background: transparent;
+	}
 </style>
 
 <script type="text/javascript">
@@ -386,7 +454,11 @@
 				following: [],
 				followingCursor: 1,
 				followingMore: true,
-				lightboxMedia: false
+				lightboxMedia: false,
+				showSuggestions: false,
+				showReadMore: true,
+				replyStatus: {},
+				replyText: '',
 			}
 		},
 
@@ -406,13 +478,27 @@
 				this.modes.dark = true;
 			}
 
+			if(localStorage.getItem('pf_metro_ui.exp.rec') == 'false') {
+				this.showSuggestions = false;
+			} else {
+				this.showSuggestions = true;
+			}
+
+			if(localStorage.getItem('pf_metro_ui.exp.rm') == 'false') {
+				this.showReadMore = false;
+			} else {
+				this.showReadMore = true;
+			}
+
 			this.$nextTick(function () {
 				$('[data-toggle="tooltip"]').tooltip()
 			});
 		},
 
 		updated() {
-			pixelfed.readmore();
+			if(this.showReadMore == true) {
+				pixelfed.readmore();
+			}
 		},
 
 		methods: {
@@ -462,9 +548,7 @@
 					this.max_id = Math.min(...ids);
 					$('.timeline .pagination').removeClass('d-none');
 					this.loading = false;
-					if(window.outerWidth > 767) {
-						this.expRec();
-					}
+					this.expRec();
 				}).catch(err => {
 				});
 			},
@@ -545,7 +629,10 @@
 					return;
 				}
 				this.replies = {};
+				this.replyStatus = {};
+				this.replyText = '';
 				this.replyId = status.id;
+				this.replyStatus = status;
 				this.fetchStatusComments(status, '');
 			},
 
@@ -677,16 +764,12 @@
 
 			commentSubmit(status, $event) {
 				let id = status.id;
-				let form = $event.target;
-				let input = $(form).find('input[name="comment"]');
-				let comment = input.val();
-				let comments = form.parentElement.parentElement.getElementsByClassName('comments')[0];
+				let comment = this.replyText;
 				axios.post('/i/comment', {
 					item: id,
 					comment: comment
 				}).then(res => {
-					form.reset();
-					form.blur();
+					this.replyText = '';
 					this.replies.push(res.data.entity);
 				});
 			},
@@ -1006,7 +1089,23 @@
 
 			ownerOrAdmin(status) {
 				return this.owner(status) || this.admin();
-			}
+			},
+
+			hideSuggestions() {
+				localStorage.setItem('pf_metro_ui.exp.rec', false);
+				this.showSuggestions = false;
+			},
+
+			emojiReaction(status) {
+				let em = event.target.innerText;
+				if(this.replyText.length == 0) {
+					this.replyText = em + ' ';
+					$('textarea[name="comment"]').focus();
+				} else {
+					this.replyText += em + ' ';
+					$('textarea[name="comment"]').focus();
+				}
+			}, 
 		}
 	}
 </script>

+ 2 - 0
resources/assets/sass/app.scss

@@ -24,3 +24,5 @@
 @import '~plyr/dist/plyr.css';
 
 @import '~vue-loading-overlay/dist/vue-loading.css';
+
+@import "moment";

+ 2 - 0
resources/assets/sass/appdark.scss

@@ -66,3 +66,5 @@ textarea {
 @import '~plyr/dist/plyr.css';
 
 @import '~vue-loading-overlay/dist/vue-loading.css';
+
+@import "moment";

+ 98 - 0
resources/assets/sass/moment.scss

@@ -0,0 +1,98 @@
+/* 
+	red
+*/
+.bg-moment-passion {
+	background: #e53935;
+	background: -webkit-linear-gradient(to left, #e35d5b, #e53935);
+	background: linear-gradient(to left, #e35d5b, #e53935);
+}
+
+/* 
+	teal/purple
+*/
+.bg-moment-azure {
+	background: #7F7FD5;
+	background: -webkit-linear-gradient(to left, #91EAE4, #86A8E7, #7F7FD5);
+	background: linear-gradient(to left, #91EAE4, #86A8E7, #7F7FD5);
+}
+
+/*
+	blue
+*/
+.bg-moment-reef {
+	background: #00d2ff;
+	background: -webkit-linear-gradient(to right, #3a7bd5, #00d2ff);
+	background: linear-gradient(to right, #3a7bd5, #00d2ff);
+}
+
+/*
+	lush green
+*/
+.bg-moment-lush {
+	background: #56ab2f;
+	background: -webkit-linear-gradient(to left, #a8e063, #56ab2f);
+	background: linear-gradient(to left, #a8e063, #56ab2f);
+}
+
+/*
+	neon green
+*/
+.bg-moment-neon {
+	background: #B3FFAB;
+	background: -webkit-linear-gradient(to right, #12FFF7, #B3FFAB);
+	background: linear-gradient(to right, #12FFF7, #B3FFAB);
+}
+
+/*
+	orange
+*/
+.bg-moment-flare {
+	background: #f12711;
+	background: -webkit-linear-gradient(to left, #f5af19, #f12711);
+	background: linear-gradient(to left, #f5af19, #f12711);
+}
+
+/*
+	orange/pink
+*/
+	.bg-moment-morning {
+	background: #FF5F6D;
+	background: -webkit-linear-gradient(to left, #FFC371, #FF5F6D);
+	background: linear-gradient(to left, #FFC371, #FF5F6D);
+}
+
+/*
+	pink
+*/
+.bg-moment-tranquil {
+	background: #EECDA3;
+	background: -webkit-linear-gradient(to right, #EF629F, #EECDA3);
+	background: linear-gradient(to right, #EF629F, #EECDA3);
+}
+
+/*
+	purple
+*/
+.bg-moment-mauve {
+	background: #42275a;
+	background: -webkit-linear-gradient(to left, #734b6d, #42275a);
+	background: linear-gradient(to left, #734b6d, #42275a);
+}
+
+/* 
+	purple
+*/
+.bg-moment-argon {
+	background: #03001e;
+	background: -webkit-linear-gradient(to left, #fdeff9, #ec38bc, #7303c0, #03001e);
+	background: linear-gradient(to left, #fdeff9, #ec38bc, #7303c0, #03001e);
+}
+
+/*
+	dark blue
+*/
+.bg-moment-royal {
+	background: #141E30;
+	background: -webkit-linear-gradient(to left, #243B55, #141E30);
+	background: linear-gradient(to left, #243B55, #141E30);
+}

+ 2 - 1
resources/lang/en/notification.php

@@ -2,7 +2,8 @@
 
 return [
 
-  'likedPhoto'          => 'liked your photo.',
+  'likedPhoto'          => 'liked your post.',
+  'likedComment'        => 'liked your comment.',
   'startedFollowingYou' => 'started following you.',
   'commented'           => 'commented on your post.',
   'mentionedYou'        => 'mentioned you.',

+ 10 - 17
resources/views/auth/passwords/reset.blade.php

@@ -5,7 +5,7 @@
     <div class="row justify-content-center">
         <div class="col-lg-5">
             <div class="card">
-                <div class="card-header">{{ __('Reset Password') }}</div>
+                <div class="card-header bg-white p-3 text-center font-weight-bold">{{ __('Reset Password') }}</div>
 
                 <div class="card-body">
                     <form method="POST" action="{{ route('password.request') }}">
@@ -14,11 +14,8 @@
                         <input type="hidden" name="token" value="{{ $token }}">
 
                         <div class="form-group row">
-                            <label for="email" class="col-md-4 col-form-label text-md-right">{{ __('E-Mail Address') }}</label>
-
-                            <div class="col-md-6">
-                                <input id="email" type="email" class="form-control{{ $errors->has('email') ? ' is-invalid' : '' }}" name="email" value="{{ $email ?? old('email') }}" required autofocus>
-
+                            <div class="col-md-12">
+                                <input id="email" type="email" class="form-control{{ $errors->has('email') ? ' is-invalid' : '' }}" name="email" value="{{ $email ?? old('email') }}" placeholder="{{ __('E-Mail Address') }}" required autofocus>
                                 @if ($errors->has('email'))
                                     <span class="invalid-feedback">
                                         <strong>{{ $errors->first('email') }}</strong>
@@ -26,12 +23,10 @@
                                 @endif
                             </div>
                         </div>
-
+                        <hr>
                         <div class="form-group row">
-                            <label for="password" class="col-md-4 col-form-label text-md-right">{{ __('Password') }}</label>
-
-                            <div class="col-md-6">
-                                <input id="password" type="password" class="form-control{{ $errors->has('password') ? ' is-invalid' : '' }}" name="password" required>
+                            <div class="col-md-12">
+                                <input id="password" type="password" class="form-control{{ $errors->has('password') ? ' is-invalid' : '' }}" name="password" placeholder="{{ __('Password') }}" required>
 
                                 @if ($errors->has('password'))
                                     <span class="invalid-feedback">
@@ -42,10 +37,8 @@
                         </div>
 
                         <div class="form-group row">
-                            <label for="password-confirm" class="col-md-4 col-form-label text-md-right">{{ __('Confirm Password') }}</label>
-
-                            <div class="col-md-6">
-                                <input id="password-confirm" type="password" class="form-control{{ $errors->has('password_confirmation') ? ' is-invalid' : '' }}" name="password_confirmation" required>
+                            <div class="col-md-12">
+                                <input id="password-confirm" type="password" class="form-control{{ $errors->has('password_confirmation') ? ' is-invalid' : '' }}" name="password_confirmation" placeholder="{{ __('Confirm Password') }}" required>
 
                                 @if ($errors->has('password_confirmation'))
                                     <span class="invalid-feedback">
@@ -56,8 +49,8 @@
                         </div>
 
                         <div class="form-group row mb-0">
-                            <div class="col-md-6 offset-md-4">
-                                <button type="submit" class="btn btn-primary">
+                            <div class="col-md-12">
+                                <button type="submit" class="btn btn-primary btn-block py-0 font-weight-bold">
                                     {{ __('Reset Password') }}
                                 </button>
                             </div>

+ 168 - 1
resources/views/settings/labs.blade.php

@@ -31,6 +31,124 @@
 			</label>
 			<p class="text-muted small help-text">MomentUI offers an alternative layout for posts and your profile.</p>
 		</div>
+		@if($profile->profile_layout == 'moment')
+		<div class="form-check pb-3">
+			<label class="form-check-label font-weight-bold mb-3" for="profile_layout">
+				{{__('MomentUI Profile Header Color')}}
+			</label>
+			<div class="row">
+				<div class="col-6 col-sm-3 pb-5">
+					<div class="">
+						<p class="form-check-label">
+							<div class="bg-pixelfed rounded-circle box-shadow" style="width:60px; height:60px"></div>
+						</p>
+						<p class="mb-0 small text-muted">Default</p>
+						<input class="form-check-input mx-0 pl-0" type="radio" name="moment_bg" value="default" {{$profile->header_bg == 'default' || !$profile->header_bg ? 'checked':''}}>
+					</div>
+				</div>
+				<div class="col-6 col-sm-3 pb-5">
+					<div class="">
+						<p class="form-check-label">
+							<div class="bg-moment-azure rounded-circle box-shadow" style="width:60px; height:60px"></div>
+						</p>
+						<p class="mb-0 small text-muted">Azure</p>
+						<input class="form-check-input mx-0" type="radio" name="moment_bg" value="azure" {{$profile->header_bg == 'azure' ? 'checked':''}}>
+					</div>
+				</div>
+				<div class="col-6 col-sm-3 pb-5">
+					<div class="">
+						<p class="form-check-label">
+							<div class="bg-moment-passion rounded-circle box-shadow" style="width:60px; height:60px"></div>
+						</p>
+						<p class="mb-0 small text-muted">Passion</p>
+						<input class="form-check-input mx-0" type="radio" name="moment_bg" value="passion" {{$profile->header_bg == 'passion' ? 'checked':''}}>
+					</div>
+				</div>
+				<div class="col-6 col-sm-3 pb-5">
+					<div class="">
+						<p class="form-check-label">
+							<div class="bg-moment-reef rounded-circle box-shadow" style="width:60px; height:60px"></div>
+						</p>
+						<p class="mb-0 small text-muted">Reef</p>
+						<input class="form-check-input mx-0" type="radio" name="moment_bg" value="reef" {{$profile->header_bg == 'reef' ? 'checked':''}}>
+					</div>
+				</div>
+				<div class="col-6 col-sm-3 pb-5">
+					<div class="">
+						<p class="form-check-label">
+							<div class="bg-moment-lush rounded-circle box-shadow" style="width:60px; height:60px"></div>
+						</p>
+						<p class="mb-0 small text-muted">Lush</p>
+						<input class="form-check-input mx-0" type="radio" name="moment_bg" value="lush" {{$profile->header_bg == 'lush' ? 'checked':''}}>
+					</div>
+				</div>
+				<div class="col-6 col-sm-3 pb-5">
+					<div class="">
+						<p class="form-check-label">
+							<div class="bg-moment-neon rounded-circle box-shadow" style="width:60px; height:60px"></div>
+						</p>
+						<p class="mb-0 small text-muted">Neon</p>
+						<input class="form-check-input mx-0" type="radio" name="moment_bg" value="neon" {{$profile->header_bg == 'neon' ? 'checked':''}}>
+					</div>
+				</div>
+				<div class="col-6 col-sm-3 pb-5">
+					<div class="">
+						<p class="form-check-label">
+							<div class="bg-moment-flare rounded-circle box-shadow" style="width:60px; height:60px"></div>
+						</p>
+						<p class="mb-0 small text-muted">Flare</p>
+						<input class="form-check-input mx-0" type="radio" name="moment_bg" value="flare" {{$profile->header_bg == 'flare' ? 'checked':''}}>
+					</div>
+				</div>
+				<div class="col-6 col-sm-3 pb-5">
+					<div class="">
+						<p class="form-check-label">
+							<div class="bg-moment-morning rounded-circle box-shadow" style="width:60px; height:60px"></div>
+						</p>
+						<p class="mb-0 small text-muted">Morning</p>
+						<input class="form-check-input mx-0" type="radio" name="moment_bg" value="morning" {{$profile->header_bg == 'morning' ? 'checked':''}}>
+					</div>
+				</div>
+				<div class="col-6 col-sm-3 pb-5">
+					<div class="">
+						<p class="form-check-label">
+							<div class="bg-moment-tranquil rounded-circle box-shadow" style="width:60px; height:60px"></div>
+						</p>
+						<p class="mb-0 small text-muted">Tranquil</p>
+						<input class="form-check-input mx-0" type="radio" name="moment_bg" value="tranquil" {{$profile->header_bg == 'tranquil' ? 'checked':''}}>
+					</div>
+				</div>
+				<div class="col-6 col-sm-3 pb-5">
+					<div class="">
+						<p class="form-check-label">
+							<div class="bg-moment-mauve rounded-circle box-shadow" style="width:60px; height:60px"></div>
+						</p>
+						<p class="mb-0 small text-muted">Mauve</p>
+						<input class="form-check-input mx-0" type="radio" name="moment_bg" value="mauve" {{$profile->header_bg == 'mauve' ? 'checked':''}}>
+					</div>
+				</div>
+				<div class="col-6 col-sm-3 pb-5">
+					<div class="">
+						<p class="form-check-label">
+							<div class="bg-moment-argon rounded-circle box-shadow" style="width:60px; height:60px"></div>
+						</p>
+						<p class="mb-0 small text-muted">Argon</p>
+						<input class="form-check-input mx-0" type="radio" name="moment_bg" value="argon" {{$profile->header_bg == 'argon' ? 'checked':''}}>
+					</div>
+				</div>
+				<div class="col-6 col-sm-3 pb-5">
+					<div class="">
+						<p class="form-check-label">
+							<div class="bg-moment-royal rounded-circle box-shadow" style="width:60px; height:60px"></div>
+						</p>
+						<p class="mb-0 small text-muted">Royal</p>
+						<input class="form-check-input mx-0" type="radio" name="moment_bg" value="royal" {{$profile->header_bg == 'royal' ? 'checked':''}}>
+					</div>
+				</div>
+			</div>
+			<p class="text-muted small help-text">Set your MomentUI profile background color. Adding a custom header image will be supported in a future version.</p>
+		</div>
+		@endif
 		<div class="form-check pb-3">
 			<input class="form-check-input" type="checkbox" name="dark_mode" id="dark_mode" {{request()->hasCookie('dark-mode') ? 'checked':''}}>
 			<label class="form-check-label font-weight-bold" for="dark_mode">
@@ -38,6 +156,22 @@
 			</label>
 			<p class="text-muted small help-text">Use dark mode theme.</p>
 		</div>
+		@if(config('exp.rec') == true)
+		<div class="form-check pb-3">
+			<input class="form-check-input" type="checkbox" name="show_suggestions" id="show_suggestions">
+			<label class="form-check-label font-weight-bold" for="show_suggestions">
+				{{__('Profile Suggestions')}}
+			</label>
+			<p class="text-muted small help-text">Show Profile Suggestions.</p>
+		</div>
+		@endif
+		<div class="form-check pb-3">
+			<input class="form-check-input" type="checkbox" name="show_readmore" id="show_readmore">
+			<label class="form-check-label font-weight-bold" for="show_readmore">
+				{{__('Use Read More')}}
+			</label>
+			<p class="text-muted small help-text">Collapses captions/comments more than 3 lines.</p>
+		</div>
 		<div class="py-3">
 			<p class="font-weight-bold text-muted text-center">Discovery</p>
 			<hr>
@@ -58,4 +192,37 @@
 			</div>
 		</div>
 	</form>
-	@endsection
+	@endsection
+
+@push('scripts')
+<script type="text/javascript">
+$(document).ready(function() {
+	let showSuggestions = localStorage.getItem('pf_metro_ui.exp.rec') == 'false' ? false : true;
+	let showReadMore = localStorage.getItem('pf_metro_ui.exp.rm') == 'false' ? false : true;
+
+	if(showSuggestions == true) {
+		$('#show_suggestions').attr('checked', true);
+	}
+
+	if(showReadMore == true) {
+		$('#show_readmore').attr('checked', true);
+	}
+
+	$('#show_suggestions').on('change', function(e) {
+		if(e.target.checked) {
+			localStorage.removeItem('pf_metro_ui.exp.rec');
+		} else {
+			localStorage.setItem('pf_metro_ui.exp.rec', false);
+		}
+	});
+
+	$('#show_readmore').on('change', function(e) {
+		if(e.target.checked) {
+			localStorage.removeItem('pf_metro_ui.exp.rm');
+		} else {
+			localStorage.setItem('pf_metro_ui.exp.rm', false);
+		}
+	});
+});
+</script>
+@endpush