Просмотр исходного кода

Merge branch 'dev' into patch-3

Stasiek Michalski 7 лет назад
Родитель
Сommit
47a15c89b0
47 измененных файлов с 1510 добавлено и 271 удалено
  1. 1 0
      .dockerignore
  2. 26 12
      Dockerfile
  3. 3 1
      app/Http/Controllers/AccountController.php
  4. 32 0
      app/Http/Controllers/ApiController.php
  5. 11 1
      app/Http/Controllers/CommentController.php
  6. 17 8
      app/Http/Controllers/LikeController.php
  7. 6 1
      app/Http/Controllers/ProfileController.php
  8. 6 4
      app/Http/Controllers/StatusController.php
  9. 12 2
      app/Http/Controllers/TimelineController.php
  10. 69 0
      app/Jobs/CommentPipeline/CommentPipeline.php
  11. 2 0
      app/Jobs/FollowPipeline/FollowPipeline.php
  12. 2 0
      app/Jobs/LikePipeline/LikePipeline.php
  13. 10 0
      app/Notification.php
  14. 5 1
      app/Providers/AppServiceProvider.php
  15. 19 1
      app/Status.php
  16. 16 0
      app/Util/Lexer/PrettyNumber.php
  17. 10 31
      app/Util/Media/Image.php
  18. 1 0
      composer.json
  19. 358 1
      composer.lock
  20. 20 0
      config/pixelfed.php
  21. 31 0
      database/migrations/2018_06_04_061435_update_notifications_table_add_polymorphic_relationship.php
  22. 40 15
      docker-compose.yml
  23. BIN
      public/css/app.css
  24. BIN
      public/js/app.js
  25. BIN
      public/js/timeline.js
  26. BIN
      public/mix-manifest.json
  27. 4 2
      resources/assets/js/bootstrap.js
  28. 29 10
      resources/assets/js/components/commentform.js
  29. 51 20
      resources/assets/js/components/likebutton.js
  30. 124 0
      resources/assets/js/lib/fontawesome-all.js
  31. 3 0
      resources/assets/js/timeline.js
  32. 1 0
      resources/assets/sass/app.scss
  33. 84 23
      resources/assets/sass/custom.scss
  34. 345 0
      resources/assets/sass/lib/fontawesome.scss
  35. 1 0
      resources/lang/en/notification.php
  36. 20 0
      resources/views/account/activity.blade.php
  37. 2 2
      resources/views/layouts/app.blade.php
  38. 7 5
      resources/views/layouts/partial/nav.blade.php
  39. 7 7
      resources/views/profile/followers.blade.php
  40. 6 6
      resources/views/profile/following.blade.php
  41. 24 12
      resources/views/profile/show.blade.php
  42. 68 66
      resources/views/status/show.blade.php
  43. 8 6
      resources/views/status/template.blade.php
  44. 23 0
      resources/views/timeline/partial/new-form.blade.php
  45. 2 17
      resources/views/timeline/personal.blade.php
  46. 2 17
      resources/views/timeline/public.blade.php
  47. 2 0
      routes/web.php

+ 1 - 0
.dockerignore

@@ -1,6 +1,7 @@
 storage
 data
 Dockerfile
+docker-compose*.yml
 .dockerignore
 .git
 .gitignore

+ 26 - 12
Dockerfile

@@ -1,17 +1,31 @@
-FROM php:7.2-fpm-alpine
+FROM php:7.2.6-fpm-alpine
 
-RUN apk add --no-cache git imagemagick \
-    && apk add --no-cache --virtual .build build-base autoconf imagemagick-dev libtool \
-    && docker-php-ext-install pdo_mysql pcntl \
-    && pecl install imagick \
-    && docker-php-ext-enable imagick \
-    && apk del --purge .build
+ARG COMPOSER_VERSION="1.6.5"
+ARG COMPOSER_CHECKSUM="67bebe9df9866a795078bb2cf21798d8b0214f2e0b2fd81f2e907a8ef0be3434"
 
-RUN curl -sS https://getcomposer.org/installer | php \
-    && mv composer.phar /usr/local/bin/ \
-    && ln -s /usr/local/bin/composer.phar /usr/local/bin/composer
+RUN apk add --no-cache --virtual .build build-base autoconf imagemagick-dev libtool && \
+  apk --no-cache add imagemagick git && \
+  docker-php-ext-install pdo_mysql pcntl && \
+  pecl install imagick && \
+  docker-php-ext-enable imagick pcntl imagick && \
+  curl -LsS https://getcomposer.org/download/${COMPOSER_VERSION}/composer.phar -o /tmp/composer.phar && \
+  echo "${COMPOSER_CHECKSUM}  /tmp/composer.phar" | sha256sum -c - && \
+  install -m0755 -o root -g root /tmp/composer.phar /usr/bin/composer.phar && \
+  ln -sf /usr/bin/composer.phar /usr/bin/composer && \
+  rm /tmp/composer.phar && \
+  apk --no-cache del --purge .build
+
+COPY . /var/www/html/
 
 WORKDIR /var/www/html
-COPY . .
-RUN composer install --prefer-source --no-interaction
+RUN install -d -m0755 -o www-data -g www-data \
+    /var/www/html/storage \
+    /var/www/html/storage/framework \
+    /var/www/html/storage/logs \
+    /var/www/html/storage/framework/sessions \
+    /var/www/html/storage/framework/views \
+    /var/www/html/storage/framework/cache && \
+  composer install --prefer-source --no-interaction
+
+VOLUME ["/var/www/html"]
 ENV PATH="~/.composer/vendor/bin:./vendor/bin:${PATH}"

+ 3 - 1
app/Http/Controllers/AccountController.php

@@ -16,7 +16,9 @@ class AccountController extends Controller
     public function notifications(Request $request)
     {
       $profile = Auth::user()->profile;
-      $notifications = $this->fetchNotifications($profile->id);
+      //$notifications = $this->fetchNotifications($profile->id);
+      $notifications = Notification::whereProfileId($profile->id)
+          ->orderBy('id','desc')->take(30)->simplePaginate();
       return view('account.activity', compact('profile', 'notifications'));
     }
 

+ 32 - 0
app/Http/Controllers/ApiController.php

@@ -0,0 +1,32 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use Auth;
+use App\Like;
+use Illuminate\Http\Request;
+
+class ApiController extends Controller
+{
+    public function __construct()
+    {
+        $this->middleware('auth');
+    }
+
+    public function hydrateLikes(Request $request)
+    {
+        $this->validate($request, [
+            'min' => 'nullable|integer|min:1',
+            'max' => 'nullable|integer',
+        ]);
+
+        $profile = Auth::user()->profile;
+
+        $likes = Like::whereProfileId($profile->id)
+                 ->orderBy('id', 'desc')
+                 ->take(1000)
+                 ->pluck('status_id');
+
+        return response()->json($likes);
+    }
+}

+ 11 - 1
app/Http/Controllers/CommentController.php

@@ -3,12 +3,21 @@
 namespace App\Http\Controllers;
 
 use Illuminate\Http\Request;
+use App\Jobs\CommentPipeline\CommentPipeline;
 use App\Jobs\StatusPipeline\NewStatusPipeline;
 use Auth, Hashids;
 use App\{Comment, Profile, Status};
 
 class CommentController extends Controller
 {
+
+    public function show(Request $request, $username, int $id, int $cid)
+    {
+      $user = Profile::whereUsername($username)->firstOrFail();
+      $status = Status::whereProfileId($user->id)->whereInReplyToId($id)->findOrFail($cid);
+      return view('status.reply', compact('user', 'status'));
+    }
+
     public function store(Request $request)
     {
       if(Auth::check() === false) { abort(403); }
@@ -32,9 +41,10 @@ class CommentController extends Controller
       $reply->save();
 
       NewStatusPipeline::dispatch($reply, false);
+      CommentPipeline::dispatch($status, $reply);
 
       if($request->ajax()) {
-        $response = ['code' => 200, 'msg' => 'Comment saved'];
+        $response = ['code' => 200, 'msg' => 'Comment saved', 'username' => $profile->username, 'url' => $reply->url(), 'profile' => $profile->url()];
       } else {
         $response = redirect($status->url());
       }

+ 17 - 8
app/Http/Controllers/LikeController.php

@@ -21,21 +21,30 @@ class LikeController extends Controller
       ]);
 
       $profile = Auth::user()->profile;
-      $status = Status::findOrFail($request->input('item'));
+      $status = Status::withCount('likes')->findOrFail($request->input('item'));
+
+      $count = $status->likes_count;
 
       if($status->likes()->whereProfileId($profile->id)->count() !== 0) {
         $like = Like::whereProfileId($profile->id)->whereStatusId($status->id)->firstOrFail();
         $like->delete();
-        return redirect()->back();
+        $count--;
+      } else {
+        $like = new Like;
+        $like->profile_id = $profile->id;
+        $like->status_id = $status->id;
+        $like->save();
+        $count++;
       }
 
-      $like = new Like;
-      $like->profile_id = $profile->id;
-      $like->status_id = $status->id;
-      $like->save();
-
       LikePipeline::dispatch($like);
 
-      return redirect($status->url());
+      if($request->ajax()) {
+        $response = ['code' => 200, 'msg' => 'Like saved', 'count' => $count];
+      } else {
+        $response = redirect($status->url());
+      }
+
+      return $response;
     }
 }

+ 6 - 1
app/Http/Controllers/ProfileController.php

@@ -32,7 +32,12 @@ class ProfileController extends Controller
       // TODO: refactor this mess
       $owner = Auth::check() && Auth::id() === $user->user_id;
       $following = ($owner == false && Auth::check()) ? $user->followedBy(Auth::user()->profile) : false;
-      $timeline = $user->statuses()->whereHas('media')->whereNull('in_reply_to_id')->orderBy('id','desc')->paginate(21);
+      $timeline = $user->statuses()
+                  ->whereHas('media')
+                  ->whereNull('in_reply_to_id')
+                  ->orderBy('id','desc')
+                  ->withCount(['comments', 'likes'])
+                  ->simplePaginate(21);
 
       return view('profile.show', compact('user', 'owner', 'following', 'timeline'));
     }

+ 6 - 4
app/Http/Controllers/StatusController.php

@@ -13,9 +13,11 @@ class StatusController extends Controller
     public function show(Request $request, $username, int $id)
     {
       $user = Profile::whereUsername($username)->firstOrFail();
-      $status = Status::whereProfileId($user->id)->findOrFail($id);
+      $status = Status::whereProfileId($user->id)
+              ->withCount('likes')
+              ->findOrFail($id);
       if(!$status->media_path && $status->in_reply_to_id) {
-        return view('status.reply', compact('user', 'status'));
+        return redirect($status->url());
       }
       return view('status.show', compact('user', 'status'));
     }
@@ -30,8 +32,8 @@ class StatusController extends Controller
       $user = Auth::user();
 
       $this->validate($request, [
-        'photo'   => 'required|image|max:15000',
-        'caption' => 'string|max:150'
+        'photo'   => 'required|mimes:jpeg,png,bmp,gif|max:' . config('pixelfed.max_photo_size'),
+        'caption' => 'string|max:' . config('pixelfed.max_caption_length')
       ]);
 
       $monthHash = hash('sha1', date('Y') . date('m'));

+ 12 - 2
app/Http/Controllers/TimelineController.php

@@ -17,14 +17,24 @@ class TimelineController extends Controller
     {
       // TODO: Use redis for timelines
       $following = Follower::whereProfileId(Auth::user()->profile->id)->pluck('following_id');
-      $timeline = Status::whereHas('media')->whereNull('in_reply_to_id')->whereIn('profile_id', $following)->orderBy('id','desc')->simplePaginate(10);
+      $following->push(Auth::user()->profile->id);
+      $timeline = Status::whereHas('media')
+                  ->whereNull('in_reply_to_id')
+                  ->whereIn('profile_id', $following)
+                  ->orderBy('id','desc')
+                  ->withCount(['comments', 'likes'])
+                  ->simplePaginate(10);
       return view('timeline.personal', compact('timeline'));
     }
 
     public function local()
     {
       // TODO: Use redis for timelines
-      $timeline = Status::whereHas('media')->whereNull('in_reply_to_id')->orderBy('id','desc')->simplePaginate(10);
+      $timeline = Status::whereHas('media')
+                  ->whereNull('in_reply_to_id')
+                  ->orderBy('id','desc')
+                  ->withCount(['comments', 'likes'])
+                  ->simplePaginate(10);
       return view('timeline.public', compact('timeline'));
     }
 

+ 69 - 0
app/Jobs/CommentPipeline/CommentPipeline.php

@@ -0,0 +1,69 @@
+<?php
+
+namespace App\Jobs\CommentPipeline;
+
+use Cache, Log, Redis;
+use App\{Like, Notification, Status};
+use App\Util\Lexer\Hashtag as HashtagLexer;
+use Illuminate\Bus\Queueable;
+use Illuminate\Queue\SerializesModels;
+use Illuminate\Queue\InteractsWithQueue;
+use Illuminate\Contracts\Queue\ShouldQueue;
+use Illuminate\Foundation\Bus\Dispatchable;
+
+class CommentPipeline implements ShouldQueue
+{
+    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
+
+    protected $status;
+    protected $comment;
+
+    /**
+     * Create a new job instance.
+     *
+     * @return void
+     */
+    public function __construct(Status $status, Status $comment)
+    {
+        $this->status = $status;
+        $this->comment = $comment;
+    }
+
+    /**
+     * Execute the job.
+     *
+     * @return void
+     */
+    public function handle()
+    {
+        $status = $this->status;
+        $comment = $this->comment;
+
+        $target = $status->profile;
+        $actor = $comment->profile;
+
+        try {
+
+            $notification = new Notification;
+            $notification->profile_id = $target->id;
+            $notification->actor_id = $actor->id;
+            $notification->action = 'comment';
+            $notification->message = $comment->replyToText();
+            $notification->rendered = $comment->replyToHtml();
+            $notification->item_id = $comment->id;
+            $notification->item_type = "App\Status";
+            $notification->save();
+
+            Cache::forever('notification.' . $notification->id, $notification);
+            
+            $redis = Redis::connection();
+
+            $nkey = config('cache.prefix').':user.' . $target->id . '.notifications';
+            $redis->lpush($nkey, $notification->id);
+
+        } catch (Exception $e) {
+            Log::error($e);
+        }
+
+    }
+}

+ 2 - 0
app/Jobs/FollowPipeline/FollowPipeline.php

@@ -46,6 +46,8 @@ class FollowPipeline implements ShouldQueue
             $notification->action = 'follow';
             $notification->message = $follower->toText();
             $notification->rendered = $follower->toHtml();
+            $notification->item_id = $target->id;
+            $notification->item_type = "App\Profile";
             $notification->save();
 
             Cache::forever('notification.' . $notification->id, $notification);

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

@@ -49,6 +49,8 @@ class LikePipeline implements ShouldQueue
             $notification->action = 'like';
             $notification->message = $like->toText();
             $notification->rendered = $like->toHtml();
+            $notification->item_id = $status->id;
+            $notification->item_type = "App\Status";
             $notification->save();
 
             Cache::forever('notification.' . $notification->id, $notification);

+ 10 - 0
app/Notification.php

@@ -17,4 +17,14 @@ class Notification extends Model
     return $this->belongsTo(Profile::class, 'profile_id', 'id');
   }
 
+  public function item()
+  {
+    return $this->morphTo();
+  }
+
+  public function status()
+  {
+    return $this->belongsTo(Status::class, 'item_id', 'id');
+  }
+
 }

+ 5 - 1
app/Providers/AppServiceProvider.php

@@ -40,7 +40,6 @@ class AppServiceProvider extends ServiceProvider
         });
 
         Blade::directive('prettySize', function($expression) {
-
             $size = intval($expression);
             $precision = 0;
             $short = true;
@@ -51,6 +50,11 @@ class AppServiceProvider extends ServiceProvider
             $res = round($size, $precision).$units[$i];
             return "<?php echo '$res'; ?>";
         });
+
+        Blade::directive('maxFileSize', function() {
+          $value = config('pixelfed.max_photo_size');
+          return \App\Util\Lexer\PrettyNumber::size($value, true);
+        });
     }
 
     /**

+ 19 - 1
app/Status.php

@@ -32,7 +32,12 @@ class Status extends Model
     {
       $id = $this->id;
       $username = $this->profile->username;
-      return url(config('app.url') . "/p/{$username}/{$id}");
+      $path = config('app.url') . "/p/{$username}/{$id}";
+      if(!is_null($this->in_reply_to_id)) {
+        $pid = $this->in_reply_to_id;
+        $path = config('app.url') . "/p/{$username}/{$pid}/c/{$id}";
+      }
+      return url($path);
     }
 
     public function mediaUrl()
@@ -96,4 +101,17 @@ class Status extends Model
       return $obj;
     }
 
+    public function replyToText()
+    {
+      $actorName = $this->profile->username;
+      return "{$actorName} " . __('notification.commented');
+    }
+
+    public function replyToHtml()
+    {
+      $actorName = $this->profile->username;
+      $actorUrl = $this->profile->url();
+      return "<a href='{$actorUrl}' class='profile-link'>{$actorName}</a> " .
+          __('notification.commented');
+    }
 }

+ 16 - 0
app/Util/Lexer/PrettyNumber.php

@@ -17,4 +17,20 @@ class PrettyNumber {
       return $expression;
   }
 
+  public static function size($expression, $kb = false)
+  {
+      if($kb) {
+        $expression = $expression * 1024;
+      }
+      $size = intval($expression);
+      $precision = 0;
+      $short = true;
+      $units = $short ?
+          ['B','k','M','G','T','P','E','Z','Y'] :
+          ['B','kB','MB','GB','TB','PB','EB','ZB','YB'];
+      for($i = 0; ($size / 1024) > 0.9; $i++, $size /= 1024) {}
+      $res = round($size, $precision).$units[$i];
+      return $res;
+  }
+
 }

+ 10 - 31
app/Util/Media/Image.php

@@ -105,42 +105,25 @@ class Image {
     $orientation = $ratio['orientation'];
 
     try {
-      $img = Intervention::make($file);
+      $img = Intervention::make($file)->orientate();
       $img->resize($aspect['width'], $aspect['height'], function ($constraint) {
         $constraint->aspectRatio();
       });
-      $converted = $this->convertPngToJpeg($path, $thumbnail, $img->extension);
+      $converted = $this->setBaseName($path, $thumbnail, $img->extension);
       $newPath = storage_path('app/'.$converted['path']);
-      $is_png = false;
-
-      if($img->extension == 'png' || $converted['png'] == true) {
-        \Log::info('PNG detected, ' . json_encode([$img, $converted]));
-        $is_png = true;
-        $newPath = str_replace('.png', '.jpeg', $newPath);
-        $img->encode('jpg', 80)->save($newPath);
-        if(!$thumbnail) {
-          @unlink($file);
-        }
-        \Log::info('PNG SAVED, ' . json_encode([$img, $newPath]));
-      } else {
-        \Log::info('PNG not detected, ' . json_encode([$img, $converted]));
-        $img->save($newPath, 75);
-      }
+            
+      $img->save($newPath, 75);
       
       if(!$thumbnail) {
         $media->orientation = $orientation;
       }
 
-      if($is_png == true) {
-        if($thumbnail == false) {
+      if($thumbnail == true) {
+          $media->thumbnail_path = $converted['path'];
+          $media->thumbnail_url = url(Storage::url($converted['path']));
+      } else {
           $media->media_path = $converted['path'];
           $media->mime = $img->mime;
-        }
-      }
-
-      if($thumbnail == true) {
-        $media->thumbnail_path = $converted['path'];
-        $media->thumbnail_url = url(Storage::url($converted['path']));
       }
 
       $media->save();
@@ -150,18 +133,14 @@ class Image {
     }
   }
 
-  public function convertPngToJpeg($basePath, $thumbnail = false, $extension)
+  public function setBaseName($basePath, $thumbnail = false, $extension)
   {
     $png = false;
     $path = explode('.', $basePath);
     $name = ($thumbnail == true) ? $path[0] . '_thumb' : $path[0];
     $ext = last($path);
     $basePath = "{$name}.{$ext}";
-    if($extension == 'png' || $ext == 'png') {
-      $ext = 'jpeg';
-      $basePath = "{$name}.{$ext}";
-      $png = true;
-    }
+
     return ['path' => $basePath, 'png' => $png];
   }
 

+ 1 - 0
composer.json

@@ -8,6 +8,7 @@
         "php": "^7.1.3",
         "99designs/http-signatures-guzzlehttp": "^2.0",
         "bitverse/identicon": "^1.1",
+        "doctrine/dbal": "^2.7",
         "fideloper/proxy": "^4.0",
         "greggilbert/recaptcha": "dev-master",
         "intervention/image": "^2.4",

+ 358 - 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": "7be7e27683f56b7ec28eeef962cf2437",
+    "content-hash": "bb2590c6fbf5fbc46642f6cbccf7969f",
     "packages": [
         {
             "name": "99designs/http-signatures",
@@ -244,6 +244,363 @@
             "description": "implementation of xdg base directory specification for php",
             "time": "2014-10-24T07:27:01+00:00"
         },
+        {
+            "name": "doctrine/annotations",
+            "version": "v1.6.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/doctrine/annotations.git",
+                "reference": "c7f2050c68a9ab0bdb0f98567ec08d80ea7d24d5"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/doctrine/annotations/zipball/c7f2050c68a9ab0bdb0f98567ec08d80ea7d24d5",
+                "reference": "c7f2050c68a9ab0bdb0f98567ec08d80ea7d24d5",
+                "shasum": ""
+            },
+            "require": {
+                "doctrine/lexer": "1.*",
+                "php": "^7.1"
+            },
+            "require-dev": {
+                "doctrine/cache": "1.*",
+                "phpunit/phpunit": "^6.4"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.6.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Doctrine\\Common\\Annotations\\": "lib/Doctrine/Common/Annotations"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Roman Borschel",
+                    "email": "roman@code-factory.org"
+                },
+                {
+                    "name": "Benjamin Eberlei",
+                    "email": "kontakt@beberlei.de"
+                },
+                {
+                    "name": "Guilherme Blanco",
+                    "email": "guilhermeblanco@gmail.com"
+                },
+                {
+                    "name": "Jonathan Wage",
+                    "email": "jonwage@gmail.com"
+                },
+                {
+                    "name": "Johannes Schmitt",
+                    "email": "schmittjoh@gmail.com"
+                }
+            ],
+            "description": "Docblock Annotations Parser",
+            "homepage": "http://www.doctrine-project.org",
+            "keywords": [
+                "annotations",
+                "docblock",
+                "parser"
+            ],
+            "time": "2017-12-06T07:11:42+00:00"
+        },
+        {
+            "name": "doctrine/cache",
+            "version": "v1.7.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/doctrine/cache.git",
+                "reference": "b3217d58609e9c8e661cd41357a54d926c4a2a1a"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/doctrine/cache/zipball/b3217d58609e9c8e661cd41357a54d926c4a2a1a",
+                "reference": "b3217d58609e9c8e661cd41357a54d926c4a2a1a",
+                "shasum": ""
+            },
+            "require": {
+                "php": "~7.1"
+            },
+            "conflict": {
+                "doctrine/common": ">2.2,<2.4"
+            },
+            "require-dev": {
+                "alcaeus/mongo-php-adapter": "^1.1",
+                "mongodb/mongodb": "^1.1",
+                "phpunit/phpunit": "^5.7",
+                "predis/predis": "~1.0"
+            },
+            "suggest": {
+                "alcaeus/mongo-php-adapter": "Required to use legacy MongoDB driver"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.7.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Doctrine\\Common\\Cache\\": "lib/Doctrine/Common/Cache"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Roman Borschel",
+                    "email": "roman@code-factory.org"
+                },
+                {
+                    "name": "Benjamin Eberlei",
+                    "email": "kontakt@beberlei.de"
+                },
+                {
+                    "name": "Guilherme Blanco",
+                    "email": "guilhermeblanco@gmail.com"
+                },
+                {
+                    "name": "Jonathan Wage",
+                    "email": "jonwage@gmail.com"
+                },
+                {
+                    "name": "Johannes Schmitt",
+                    "email": "schmittjoh@gmail.com"
+                }
+            ],
+            "description": "Caching library offering an object-oriented API for many cache backends",
+            "homepage": "http://www.doctrine-project.org",
+            "keywords": [
+                "cache",
+                "caching"
+            ],
+            "time": "2017-08-25T07:02:50+00:00"
+        },
+        {
+            "name": "doctrine/collections",
+            "version": "v1.5.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/doctrine/collections.git",
+                "reference": "a01ee38fcd999f34d9bfbcee59dbda5105449cbf"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/doctrine/collections/zipball/a01ee38fcd999f34d9bfbcee59dbda5105449cbf",
+                "reference": "a01ee38fcd999f34d9bfbcee59dbda5105449cbf",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.1"
+            },
+            "require-dev": {
+                "doctrine/coding-standard": "~0.1@dev",
+                "phpunit/phpunit": "^5.7"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.3.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-0": {
+                    "Doctrine\\Common\\Collections\\": "lib/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Roman Borschel",
+                    "email": "roman@code-factory.org"
+                },
+                {
+                    "name": "Benjamin Eberlei",
+                    "email": "kontakt@beberlei.de"
+                },
+                {
+                    "name": "Guilherme Blanco",
+                    "email": "guilhermeblanco@gmail.com"
+                },
+                {
+                    "name": "Jonathan Wage",
+                    "email": "jonwage@gmail.com"
+                },
+                {
+                    "name": "Johannes Schmitt",
+                    "email": "schmittjoh@gmail.com"
+                }
+            ],
+            "description": "Collections Abstraction library",
+            "homepage": "http://www.doctrine-project.org",
+            "keywords": [
+                "array",
+                "collections",
+                "iterator"
+            ],
+            "time": "2017-07-22T10:37:32+00:00"
+        },
+        {
+            "name": "doctrine/common",
+            "version": "v2.8.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/doctrine/common.git",
+                "reference": "f68c297ce6455e8fd794aa8ffaf9fa458f6ade66"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/doctrine/common/zipball/f68c297ce6455e8fd794aa8ffaf9fa458f6ade66",
+                "reference": "f68c297ce6455e8fd794aa8ffaf9fa458f6ade66",
+                "shasum": ""
+            },
+            "require": {
+                "doctrine/annotations": "1.*",
+                "doctrine/cache": "1.*",
+                "doctrine/collections": "1.*",
+                "doctrine/inflector": "1.*",
+                "doctrine/lexer": "1.*",
+                "php": "~7.1"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^5.7"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "2.8.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Doctrine\\Common\\": "lib/Doctrine/Common"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Roman Borschel",
+                    "email": "roman@code-factory.org"
+                },
+                {
+                    "name": "Benjamin Eberlei",
+                    "email": "kontakt@beberlei.de"
+                },
+                {
+                    "name": "Guilherme Blanco",
+                    "email": "guilhermeblanco@gmail.com"
+                },
+                {
+                    "name": "Jonathan Wage",
+                    "email": "jonwage@gmail.com"
+                },
+                {
+                    "name": "Johannes Schmitt",
+                    "email": "schmittjoh@gmail.com"
+                }
+            ],
+            "description": "Common Library for Doctrine projects",
+            "homepage": "http://www.doctrine-project.org",
+            "keywords": [
+                "annotations",
+                "collections",
+                "eventmanager",
+                "persistence",
+                "spl"
+            ],
+            "time": "2017-08-31T08:43:38+00:00"
+        },
+        {
+            "name": "doctrine/dbal",
+            "version": "v2.7.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/doctrine/dbal.git",
+                "reference": "11037b4352c008373561dc6fc836834eed80c3b5"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/doctrine/dbal/zipball/11037b4352c008373561dc6fc836834eed80c3b5",
+                "reference": "11037b4352c008373561dc6fc836834eed80c3b5",
+                "shasum": ""
+            },
+            "require": {
+                "doctrine/common": "^2.7.1",
+                "ext-pdo": "*",
+                "php": "^7.1"
+            },
+            "require-dev": {
+                "doctrine/coding-standard": "^4.0",
+                "phpunit/phpunit": "^7.0",
+                "phpunit/phpunit-mock-objects": "!=3.2.4,!=3.2.5",
+                "symfony/console": "^2.0.5||^3.0",
+                "symfony/phpunit-bridge": "^3.4.5|^4.0.5"
+            },
+            "suggest": {
+                "symfony/console": "For helpful console commands such as SQL execution and import of files."
+            },
+            "bin": [
+                "bin/doctrine-dbal"
+            ],
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "2.7.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-0": {
+                    "Doctrine\\DBAL\\": "lib/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Roman Borschel",
+                    "email": "roman@code-factory.org"
+                },
+                {
+                    "name": "Benjamin Eberlei",
+                    "email": "kontakt@beberlei.de"
+                },
+                {
+                    "name": "Guilherme Blanco",
+                    "email": "guilhermeblanco@gmail.com"
+                },
+                {
+                    "name": "Jonathan Wage",
+                    "email": "jonwage@gmail.com"
+                }
+            ],
+            "description": "Database Abstraction Layer",
+            "homepage": "http://www.doctrine-project.org",
+            "keywords": [
+                "database",
+                "dbal",
+                "persistence",
+                "queryobject"
+            ],
+            "time": "2018-04-07T18:44:18+00:00"
+        },
         {
             "name": "doctrine/inflector",
             "version": "v1.3.0",

+ 20 - 0
config/pixelfed.php

@@ -76,5 +76,25 @@ return [
     'remote_follow_enabled' => env('REMOTE_FOLLOW', false),
 
     'activitypub_enabled' => env('ACTIVITY_PUB', false),
+
+    /*
+    |--------------------------------------------------------------------------
+    | Photo file size limit
+    |--------------------------------------------------------------------------
+    |
+    | Update the max photo size, in KB.
+    |
+    */
+    'max_photo_size' => env('MAX_PHOTO_SIZE', 15000),
+
+    /*
+    |--------------------------------------------------------------------------
+    | Caption limit
+    |--------------------------------------------------------------------------
+    |
+    | Change the caption length limit for new local posts.
+    |
+    */
+    'max_caption_length' => env('MAX_CAPTION_LENGTH', 150),
     
 ];

+ 31 - 0
database/migrations/2018_06_04_061435_update_notifications_table_add_polymorphic_relationship.php

@@ -0,0 +1,31 @@
+<?php
+
+use Illuminate\Support\Facades\Schema;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Database\Migrations\Migration;
+
+class UpdateNotificationsTableAddPolymorphicRelationship extends Migration
+{
+    /**
+     * Run the migrations.
+     *
+     * @return void
+     */
+    public function up()
+    {
+        Schema::table('notifications', function (Blueprint $table) {
+            $table->bigInteger('item_id')->unsigned()->nullable()->after('actor_id');
+            $table->string('item_type')->nullable()->after('item_id');
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     *
+     * @return void
+     */
+    public function down()
+    {
+        //
+    }
+}

+ 40 - 15
docker-compose.yml

@@ -3,37 +3,62 @@ version: '3'
 services:
   nginx:
     image: nginx:alpine
+    networks:
+      - internal
+      - external
     ports:
-    - 3000:80
+      - 3000:80
     volumes:
-    - .:/var/www/html
-    - ./contrib/nginx.conf:/etc/nginx/conf.d/default.conf
+      - "php-storage:/var/www/html"
+      - ./contrib/nginx.conf:/etc/nginx/conf.d/default.conf
     depends_on:
-    - php
+      - php
+
   php:
     build: .
+    image: pixelfed
     volumes:
-    - ./storage:/var/www/html/storage
-    depends_on:
-    - mysql
-    - redis
+      - "php-storage:/var/www/html"
+    networks:
+      - internal
     environment:
       - DB_HOST=mysql
       - DB_DATABASE=pixelfed
-      - DB_USERNAME=${DB_USERNAME}
-      - DB_PASSWORD=${DB_PASSWORD}
+      - DB_USERNAME=${DB_USERNAME:-pixelfed}
+      - DB_PASSWORD=${DB_PASSWORD:-pixelfed}
       - REDIS_HOST=redis
       - APP_KEY=${APP_KEY}
+    env_file:
+      - ./.env
+
   mysql:
     image: mysql:5.7
+    networks:
+      - internal
     environment:
       - MYSQL_DATABASE=pixelfed
-      - MYSQL_USER=${DB_USERNAME}
-      - MYSQL_PASSWORD=${DB_PASSWORD}
+      - MYSQL_USER=${DB_USERNAME:-pixelfed}
+      - MYSQL_PASSWORD=${DB_PASSWORD:-pixelfed}
+      - MYSQL_RANDOM_ROOT_PASSWORD="true"
+    env_file:
+      - ./.env
     volumes:
-    - ./docker-volumes/mysql:/var/lib/mysql
+      - "mysql-data:/var/lib/mysql"
+
   redis:
     image: redis:alpine
     volumes:
-    - ./docker-volumes/redis:/data
-...
+      - "redis-data:/data"
+    networks:
+      - internal
+
+volumes:
+  redis-data:
+  mysql-data:
+  php-storage:
+
+networks:
+  internal:
+    internal: true
+  external:
+    driver: bridge

BIN
public/css/app.css


BIN
public/js/app.js


BIN
public/js/timeline.js


BIN
public/mix-manifest.json


+ 4 - 2
resources/assets/js/bootstrap.js

@@ -9,6 +9,7 @@ window.Popper = require('popper.js').default;
  */
 
 try {
+    window.pixelfed = {};
     window.$ = window.jQuery = require('jquery');
     require('bootstrap');
     window.InfiniteScroll = require('infinite-scroll');
@@ -16,9 +17,10 @@ try {
     window.typeahead = require('./lib/typeahead');
     window.Bloodhound = require('./lib/bloodhound');
 
+    require('./lib/fontawesome-all');
     require('./components/localstorage');
-    //require('./components/likebutton');
-    //require('./components/commentform');
+    require('./components/likebutton');
+    require('./components/commentform');
     require('./components/searchform');
     require('./components/bookmarkform');
 } catch (e) {}

+ 29 - 10
resources/assets/js/components/commentform.js

@@ -1,6 +1,11 @@
 $(document).ready(function() {
 
-  $('.comment-form').submit(function(e, data) {
+  $('.status-comment-focus').on('click', function(el) {
+    var el = $(this).parents().eq(2).find('input[name="comment"]');
+    el.focus();
+  });
+
+  $(document).on('submit', '.comment-form', function(e, data) {
     e.preventDefault();
 
     let el = $(this);
@@ -8,18 +13,32 @@ $(document).ready(function() {
     let commentform = el.find('input[name="comment"]');
     let commenttext = commentform.val();
     let item = {item: id, comment: commenttext};
-    try {
-      axios.post('/i/comment', item);
-      var comments = el.parent().parent().find('.comments');
-      var comment = '<p class="mb-0"><span class="font-weight-bold pr-1">' + pixelfed.user.username + '</span><span class="comment-text">'+ commenttext + '</span></p>';
-      comments.prepend(comment);
 
+    axios.post('/i/comment', item)
+    .then(function (res) {
+
+      var username = res.data.username;
+      var permalink = res.data.url;
+      var profile = res.data.profile;
+
+      if($('.status-container').length == 1) {
+        var comments = el.parents().eq(3).find('.comments');
+      } else {
+        var comments = el.parents().eq(1).find('.comments');
+      }
+
+      var comment = '<p class="mb-0"><span class="font-weight-bold pr-1"><bdi><a class="text-dark" href="' + profile + '">' + username + '</a></bdi></span><span class="comment-text">'+ commenttext + '</span><span class="float-right"><a href="' + permalink + '" class="text-dark small font-weight-bold">1s</a></span></p>';
+
+      comments.prepend(comment);
+      
       commentform.val('');
       commentform.blur();
-      return true;
-    } catch(e) {
-      return false;
-    }
+
+    })
+    .catch(function (res) {
+      
+    });
+ 
   });
 
 });

+ 51 - 20
resources/assets/js/components/likebutton.js

@@ -1,29 +1,60 @@
 $(document).ready(function() {
+
   if(!ls.get('likes')) {
-    ls.set('likes', []);
+    axios.get('/api/v1/likes')
+    .then(function (res) {
+      ls.set('likes', res.data);
+      console.log(res);
+    })
+    .catch(function (res) {
+      ls.set('likes', []);
+    })
   }
 
-  $('.like-form').submit(function(e) {
+
+  pixelfed.hydrateLikes = function() {
+    var likes = ls.get('likes');
+    $('.like-form').each(function(i, el) {
+      var el = $(el);
+      var id = el.data('id');
+      var heart = el.find('.status-heart');
+
+      if(likes.indexOf(id) != -1) {
+        heart.addClass('fas fa-heart').removeClass('far fa-heart');
+      }
+    });
+  };
+
+  pixelfed.hydrateLikes();
+
+  $(document).on('submit', '.like-form', function(e) {
     e.preventDefault();
     var el = $(this);
     var id = el.data('id');
-    var res = axios.post('/i/like', {item: id});
-    var likes = ls.get('likes');
-    var action = false;
-    var counter = el.parent().parent().find('.like-count');
-    var count = parseInt(counter.text());
-    if(likes.indexOf(id) > -1) {
-      likes.splice(id, 1);
-      count--;
-      counter.text(count);
-      action = 'unlike';
-    } else {
-      likes.push(id);
-      count++;
-      counter.text(count);
-      action = 'like';
-    }
-    ls.set('likes', likes);
-    console.log(action + ' - ' + $(this).data('id') + ' like event');
+    axios.post('/i/like', {item: id})
+    .then(function (res) {
+      var likes = ls.get('likes');
+      var action = false;
+      var counter = el.parents().eq(1).find('.like-count');
+      var count = res.data.count;
+      var heart = el.find('.status-heart');
+
+      if(likes.indexOf(id) > -1) {
+        heart.addClass('far fa-heart').removeClass('fas fa-heart');
+        likes = likes.filter(function(item) { 
+            return item !== id
+        });
+        counter.text(count);
+        action = 'unlike';
+      } else {
+        heart.addClass('fas fa-heart').removeClass('far fa-heart');
+        likes.push(id);
+        counter.text(count);
+        action = 'like';
+      }
+
+      ls.set('likes', likes);
+      console.log(action + ' - ' + id + ' like event');
+    });
   });
 });

Разница между файлами не показана из-за своего большого размера
+ 124 - 0
resources/assets/js/lib/fontawesome-all.js


+ 3 - 0
resources/assets/js/timeline.js

@@ -6,4 +6,7 @@ $(document).ready(function() {
     append: '.timeline-feed',
     history: false,
   });
+  infScroll.on( 'append', function( response, path, items ) {
+    pixelfed.hydrateLikes();
+  });
 });

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

@@ -1,6 +1,7 @@
 
 // Fonts
 @import "fonts";
+@import "lib/fontawesome";
 
 // Variables
 @import "variables";

+ 84 - 23
resources/assets/sass/custom.scss

@@ -68,52 +68,113 @@ body, button, input, textarea {
 }
 
 .card.status-container .status-photo {
-  display: -webkit-box !important;
-  display: -ms-flexbox !important;
-  display: flex !important;
   margin: auto !important;
 }
 
 .card.status-container .status-comments {
-  height: 220px;
   overflow-y: scroll;
   border-bottom:1px solid rgba(0, 0, 0, 0.1);
 }
 
-.card.status-container.orientation-square .status-comments {
-  height: 360px !important;
+.no-caret.dropdown-toggle {
+  text-decoration: none !important;
 }
 
+.no-caret.dropdown-toggle::after {
+  display:none;
+}
 
-.card.status-container.orientation-portrait .status-comments {
-  height: 528px !important;
+.notification-page .profile-link {
+  color: #212529;
+  font-weight: bold;
 }
 
-.card.status-container.orientation-landscape .status-photo img {
-  max-height: 451px !important;
+.notification-page .list-group-item:first-child {
+  border-top: none;
 }
 
-.card.status-container.orientation-square .status-photo img {
-  max-height: 601px !important;
+.nav-topbar {
+  border-top: 1px solid $gray-300;
+}
+.nav-topbar .nav-item {
+  margin: -1px 1.5rem 0;
+}
+.nav-topbar .nav-link {
+  border: 1px solid transparent;
+  color: $gray-300;
+  padding: 0.75rem 0;
+}
+.nav-topbar .nav-link:focus, .nav-topbar .nav-link:hover {
+  border-top-color: $gray-300;
+}
+.nav-topbar .nav-link.disabled {
+  color: $gray-300;
+  background-color: transparent;
+  border-color: transparent;
+}
+.nav-topbar .nav-item.show .nav-link, .nav-topbar .nav-link.active {
+  color: $gray-600;
+  border-top-color: $gray-600;
+}
+.nav-topbar .dropdown-menu {
+  margin-top:-1px;
 }
 
-.card.status-container.orientation-portrait .status-photo img {
-  max-height: 772px !important;
+.info-overlay {
+    position: relative;
 }
 
-.no-caret.dropdown-toggle {
-  text-decoration: none !important;
+.info-overlay .info-overlay-text {
+    display: none;
+    position: absolute;
 }
 
-.no-caret.dropdown-toggle::after {
-  display:none;
+.info-overlay:hover .info-overlay-text {
+    display: flex;
 }
 
-.notification-page .profile-link {
-  color: #212529;
-  font-weight: bold;
+.info-overlay-text {
+    width: 100%;
+    height: 100%;
+    background-color: rgba(0,0,0,0.5);
 }
 
-.notification-page .list-group-item:first-child {
-  border-top: none;
+.font-weight-ultralight {
+    font-weight: 200 !important;
 }
+
+.square {
+    position: relative;
+    width: 100%;
+}
+
+.square::after {
+    content: "";
+    display: block;
+    padding-bottom: 100%;
+}
+
+.square-content {
+    position: absolute;
+    width: 100%;
+    height: 100%;
+    background-repeat: no-repeat;
+    background-size: cover;
+    background-position: 50%;
+}
+
+.fas, .far {
+  font-size: 25px !important;
+}
+
+
+.far.fa-heart {
+  color: #343a40;
+}
+
+.svg-inline--fa.fa-heart.fa-w-16.status-heart.fa-2x {
+  color: #f70ec4;
+}
+
+.fas.fa-heart {
+}

+ 345 - 0
resources/assets/sass/lib/fontawesome.scss

@@ -0,0 +1,345 @@
+/*!
+ * Font Awesome Free 5.0.13 by @fontawesome - https://fontawesome.com
+ * License - https://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
+ */
+svg:not(:root).svg-inline--fa {
+  overflow: visible; }
+
+.svg-inline--fa {
+  display: inline-block;
+  font-size: inherit;
+  height: 1em;
+  overflow: visible;
+  vertical-align: -.125em; }
+  .svg-inline--fa.fa-lg {
+    vertical-align: -.225em; }
+  .svg-inline--fa.fa-w-1 {
+    width: 0.0625em; }
+  .svg-inline--fa.fa-w-2 {
+    width: 0.125em; }
+  .svg-inline--fa.fa-w-3 {
+    width: 0.1875em; }
+  .svg-inline--fa.fa-w-4 {
+    width: 0.25em; }
+  .svg-inline--fa.fa-w-5 {
+    width: 0.3125em; }
+  .svg-inline--fa.fa-w-6 {
+    width: 0.375em; }
+  .svg-inline--fa.fa-w-7 {
+    width: 0.4375em; }
+  .svg-inline--fa.fa-w-8 {
+    width: 0.5em; }
+  .svg-inline--fa.fa-w-9 {
+    width: 0.5625em; }
+  .svg-inline--fa.fa-w-10 {
+    width: 0.625em; }
+  .svg-inline--fa.fa-w-11 {
+    width: 0.6875em; }
+  .svg-inline--fa.fa-w-12 {
+    width: 0.75em; }
+  .svg-inline--fa.fa-w-13 {
+    width: 0.8125em; }
+  .svg-inline--fa.fa-w-14 {
+    width: 0.875em; }
+  .svg-inline--fa.fa-w-15 {
+    width: 0.9375em; }
+  .svg-inline--fa.fa-w-16 {
+    width: 1em; }
+  .svg-inline--fa.fa-w-17 {
+    width: 1.0625em; }
+  .svg-inline--fa.fa-w-18 {
+    width: 1.125em; }
+  .svg-inline--fa.fa-w-19 {
+    width: 1.1875em; }
+  .svg-inline--fa.fa-w-20 {
+    width: 1.25em; }
+  .svg-inline--fa.fa-pull-left {
+    margin-right: .3em;
+    width: auto; }
+  .svg-inline--fa.fa-pull-right {
+    margin-left: .3em;
+    width: auto; }
+  .svg-inline--fa.fa-border {
+    height: 1.5em; }
+  .svg-inline--fa.fa-li {
+    width: 2em; }
+  .svg-inline--fa.fa-fw {
+    width: 1.25em; }
+
+.fa-layers svg.svg-inline--fa {
+  bottom: 0;
+  left: 0;
+  margin: auto;
+  position: absolute;
+  right: 0;
+  top: 0; }
+
+.fa-layers {
+  display: inline-block;
+  height: 1em;
+  position: relative;
+  text-align: center;
+  vertical-align: -.125em;
+  width: 1em; }
+  .fa-layers svg.svg-inline--fa {
+    -webkit-transform-origin: center center;
+            transform-origin: center center; }
+
+.fa-layers-text, .fa-layers-counter {
+  display: inline-block;
+  position: absolute;
+  text-align: center; }
+
+.fa-layers-text {
+  left: 50%;
+  top: 50%;
+  -webkit-transform: translate(-50%, -50%);
+          transform: translate(-50%, -50%);
+  -webkit-transform-origin: center center;
+          transform-origin: center center; }
+
+.fa-layers-counter {
+  background-color: #ff253a;
+  border-radius: 1em;
+  -webkit-box-sizing: border-box;
+          box-sizing: border-box;
+  color: #fff;
+  height: 1.5em;
+  line-height: 1;
+  max-width: 5em;
+  min-width: 1.5em;
+  overflow: hidden;
+  padding: .25em;
+  right: 0;
+  text-overflow: ellipsis;
+  top: 0;
+  -webkit-transform: scale(0.25);
+          transform: scale(0.25);
+  -webkit-transform-origin: top right;
+          transform-origin: top right; }
+
+.fa-layers-bottom-right {
+  bottom: 0;
+  right: 0;
+  top: auto;
+  -webkit-transform: scale(0.25);
+          transform: scale(0.25);
+  -webkit-transform-origin: bottom right;
+          transform-origin: bottom right; }
+
+.fa-layers-bottom-left {
+  bottom: 0;
+  left: 0;
+  right: auto;
+  top: auto;
+  -webkit-transform: scale(0.25);
+          transform: scale(0.25);
+  -webkit-transform-origin: bottom left;
+          transform-origin: bottom left; }
+
+.fa-layers-top-right {
+  right: 0;
+  top: 0;
+  -webkit-transform: scale(0.25);
+          transform: scale(0.25);
+  -webkit-transform-origin: top right;
+          transform-origin: top right; }
+
+.fa-layers-top-left {
+  left: 0;
+  right: auto;
+  top: 0;
+  -webkit-transform: scale(0.25);
+          transform: scale(0.25);
+  -webkit-transform-origin: top left;
+          transform-origin: top left; }
+
+.fa-lg {
+  font-size: 1.33333em;
+  line-height: 0.75em;
+  vertical-align: -.0667em; }
+
+.fa-xs {
+  font-size: .75em; }
+
+.fa-sm {
+  font-size: .875em; }
+
+.fa-1x {
+  font-size: 1em; }
+
+.fa-2x {
+  font-size: 2em; }
+
+.fa-3x {
+  font-size: 3em; }
+
+.fa-4x {
+  font-size: 4em; }
+
+.fa-5x {
+  font-size: 5em; }
+
+.fa-6x {
+  font-size: 6em; }
+
+.fa-7x {
+  font-size: 7em; }
+
+.fa-8x {
+  font-size: 8em; }
+
+.fa-9x {
+  font-size: 9em; }
+
+.fa-10x {
+  font-size: 10em; }
+
+.fa-fw {
+  text-align: center;
+  width: 1.25em; }
+
+.fa-ul {
+  list-style-type: none;
+  margin-left: 2.5em;
+  padding-left: 0; }
+  .fa-ul > li {
+    position: relative; }
+
+.fa-li {
+  left: -2em;
+  position: absolute;
+  text-align: center;
+  width: 2em;
+  line-height: inherit; }
+
+.fa-border {
+  border: solid 0.08em #eee;
+  border-radius: .1em;
+  padding: .2em .25em .15em; }
+
+.fa-pull-left {
+  float: left; }
+
+.fa-pull-right {
+  float: right; }
+
+.fa.fa-pull-left,
+.fas.fa-pull-left,
+.far.fa-pull-left,
+.fal.fa-pull-left,
+.fab.fa-pull-left {
+  margin-right: .3em; }
+
+.fa.fa-pull-right,
+.fas.fa-pull-right,
+.far.fa-pull-right,
+.fal.fa-pull-right,
+.fab.fa-pull-right {
+  margin-left: .3em; }
+
+.fa-spin {
+  -webkit-animation: fa-spin 2s infinite linear;
+          animation: fa-spin 2s infinite linear; }
+
+.fa-pulse {
+  -webkit-animation: fa-spin 1s infinite steps(8);
+          animation: fa-spin 1s infinite steps(8); }
+
+@-webkit-keyframes fa-spin {
+  0% {
+    -webkit-transform: rotate(0deg);
+            transform: rotate(0deg); }
+  100% {
+    -webkit-transform: rotate(360deg);
+            transform: rotate(360deg); } }
+
+@keyframes fa-spin {
+  0% {
+    -webkit-transform: rotate(0deg);
+            transform: rotate(0deg); }
+  100% {
+    -webkit-transform: rotate(360deg);
+            transform: rotate(360deg); } }
+
+.fa-rotate-90 {
+  -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";
+  -webkit-transform: rotate(90deg);
+          transform: rotate(90deg); }
+
+.fa-rotate-180 {
+  -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";
+  -webkit-transform: rotate(180deg);
+          transform: rotate(180deg); }
+
+.fa-rotate-270 {
+  -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";
+  -webkit-transform: rotate(270deg);
+          transform: rotate(270deg); }
+
+.fa-flip-horizontal {
+  -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";
+  -webkit-transform: scale(-1, 1);
+          transform: scale(-1, 1); }
+
+.fa-flip-vertical {
+  -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";
+  -webkit-transform: scale(1, -1);
+          transform: scale(1, -1); }
+
+.fa-flip-horizontal.fa-flip-vertical {
+  -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";
+  -webkit-transform: scale(-1, -1);
+          transform: scale(-1, -1); }
+
+:root .fa-rotate-90,
+:root .fa-rotate-180,
+:root .fa-rotate-270,
+:root .fa-flip-horizontal,
+:root .fa-flip-vertical {
+  -webkit-filter: none;
+          filter: none; }
+
+.fa-stack {
+  display: inline-block;
+  height: 2em;
+  position: relative;
+  width: 2em; }
+
+.fa-stack-1x,
+.fa-stack-2x {
+  bottom: 0;
+  left: 0;
+  margin: auto;
+  position: absolute;
+  right: 0;
+  top: 0; }
+
+.svg-inline--fa.fa-stack-1x {
+  height: 1em;
+  width: 1em; }
+
+.svg-inline--fa.fa-stack-2x {
+  height: 2em;
+  width: 2em; }
+
+.fa-inverse {
+  color: #fff; }
+
+.sr-only {
+  border: 0;
+  clip: rect(0, 0, 0, 0);
+  height: 1px;
+  margin: -1px;
+  overflow: hidden;
+  padding: 0;
+  position: absolute;
+  width: 1px; }
+
+.sr-only-focusable:active, .sr-only-focusable:focus {
+  clip: auto;
+  height: auto;
+  margin: 0;
+  overflow: visible;
+  position: static;
+  width: auto; }

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

@@ -4,5 +4,6 @@ return [
 
   'likedPhoto' => 'liked your photo.',
   'startedFollowingYou' => 'started following you.',
+  'commented' => 'commented on your post.',
 
 ];

+ 20 - 0
resources/views/account/activity.blade.php

@@ -18,8 +18,12 @@
             <span class="text-muted notification-timestamp pl-1">{{$notification->created_at->diffForHumans(null, true, true, true)}}</span>
           </span>
           <span class="float-right notification-action">
+            @if($notification->item_id)
+              <a href="{{$notification->status->url()}}"><img src="{{$notification->status->thumb()}}" width="32px" height="32px"></a>
+            @endif
           </span>
         @break
+
         @case('follow')
           <span class="notification-icon pr-3">
             <img src="{{$notification->actor->avatarUrl()}}" width="32px" class="rounded-circle">
@@ -38,6 +42,22 @@
           </span>
           @endif
         @break
+
+        @case('comment')
+          <span class="notification-icon pr-3">
+            <img src="{{$notification->actor->avatarUrl()}}" width="32px" class="rounded-circle">
+          </span>
+          <span class="notification-text">
+            {!! $notification->rendered !!}
+            <span class="text-muted notification-timestamp pl-1">{{$notification->created_at->diffForHumans(null, true, true, true)}}</span>
+          </span>
+          <span class="float-right notification-action">
+            @if($notification->item_id)
+              <a href="{{$notification->status->parent()->url()}}"><img src="{{$notification->status->parent()->thumb()}}" width="32px" height="32px"></a>
+            @endif
+          </span>
+        @break
+
         @endswitch
       </li>
       @endforeach

+ 2 - 2
resources/views/layouts/app.blade.php

@@ -14,7 +14,7 @@
     <link rel="dns-prefetch" href="https://fonts.gstatic.com">
     <link rel="dns-prefetch" href="https://cdnjs.cloudflare.com">
     <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/simple-line-icons/2.4.1/css/simple-line-icons.min.css" integrity="sha256-7O1DfUu4pybYI7uAATw34eDrgQaWGOfMV/8erfDQz/Q=" crossorigin="anonymous" />
-    <link href="{{ asset('css/app.css') }}" rel="stylesheet">
+    <link href="{{ mix('css/app.css') }}" rel="stylesheet">
     @stack('styles')
 </head>
 <body class="">
@@ -23,7 +23,7 @@
         @yield('content')
     </main>
     @include('layouts.partial.footer')
-    <script type="text/javascript" src="{{ asset('js/app.js') }}"></script>
+    <script type="text/javascript" src="{{ mix('js/app.js') }}"></script>
     @stack('scripts')
 </body>
 </html>

+ 7 - 5
resources/views/layouts/partial/nav.blade.php

@@ -1,15 +1,12 @@
-<nav class="navbar navbar-expand-md navbar-light navbar-laravel">
+<nav class="navbar navbar-expand navbar-light navbar-laravel sticky-top">
     <div class="container">
         <a class="navbar-brand d-flex align-items-center" href="{{ url('/timeline') }}">
             <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mr-2"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"></path><circle cx="12" cy="13" r="4"></circle></svg>
             <strong class="font-weight-bold">{{ config('app.name', 'Laravel') }}</strong>
         </a>
-        <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
-            <span class="navbar-toggler-icon"></span>
-        </button>
 
         <div class="collapse navbar-collapse" id="navbarSupportedContent">
-            <ul class="navbar-nav ml-auto">
+            <ul class="navbar-nav ml-auto d-none d-md-block">
               <form class="form-inline search-form">
                 <input class="form-control mr-sm-2 search-form-input" type="search" placeholder="Search" aria-label="Search">
               </form>
@@ -64,3 +61,8 @@
         </div>
     </div>
 </nav>
+<nav class="breadcrumb d-md-none d-flex">
+  <form class="form-inline search-form mx-auto">
+   <input class="form-control mr-sm-2 search-form-input" type="search" placeholder="Search" aria-label="Search">
+  </form>
+</nav>

+ 7 - 7
resources/views/profile/followers.blade.php

@@ -4,16 +4,16 @@
 
 <div class="container following-page" style="min-height: 60vh;">
 
-  <div class="profile-header row my-5 offset-md-1">
-    <div class="col-12 col-md-3">
-      <div class="profile-avatar">
+  <div class="profile-header row my-5">
+    <div class="col-12 col-md-4 d-flex">
+      <div class="profile-avatar mx-auto">
         <img class="img-thumbnail" src="{{$profile->avatarUrl()}}" style="border-radius:100%;" width="172px">
       </div>
     </div>
-    <div class="col-12 col-md-9 d-flex align-items-center">
+    <div class="col-12 col-md-8 d-flex align-items-center">
       <div class="profile-details">
-        <div class="username-bar pb-2  d-flex align-items-center">
-          <span class="font-weight-light h1">{{$profile->username}}</span>
+        <div class="username-bar pb-2 d-flex align-items-center">
+          <span class="font-weight-ultralight h1">{{$profile->username}}</span>
         </div>
         <div class="profile-stats pb-3 d-inline-flex lead">
           <div class="font-weight-light pr-5">
@@ -42,7 +42,7 @@
     </div>
   </div>
 
-  <div class="col-12 col-md-8 offset-2">
+  <div class="col-12 col-md-8 offset-md-2">
     @if($followers->count() !== 0)
     <ul class="list-group mt-4">
       @foreach($followers as $user)

+ 6 - 6
resources/views/profile/following.blade.php

@@ -4,16 +4,16 @@
 
 <div class="container following-page" style="min-height: 60vh;">
 
-  <div class="profile-header row my-5 offset-md-1">
-    <div class="col-12 col-md-3">
-      <div class="profile-avatar">
+  <div class="profile-header row my-5">
+    <div class="col-12 col-md-4 d-flex">
+      <div class="profile-avatar mx-auto">
         <img class="img-thumbnail" src="{{$profile->avatarUrl()}}" style="border-radius:100%;" width="172px">
       </div>
     </div>
-    <div class="col-12 col-md-9 d-flex align-items-center">
+    <div class="col-12 col-md-8 d-flex align-items-center">
       <div class="profile-details">
         <div class="username-bar pb-2  d-flex align-items-center">
-          <span class="font-weight-light h1">{{$profile->username}}</span>
+          <span class="font-weight-ultralight h1">{{$profile->username}}</span>
         </div>
         <div class="profile-stats pb-3 d-inline-flex lead">
           <div class="font-weight-light pr-5">
@@ -42,7 +42,7 @@
     </div>
   </div>
 
-  <div class="col-12 col-md-8 offset-2">
+  <div class="col-12 col-md-8 offset-md-2">
     @if($following->count() !== 0)
     <ul class="list-group mt-4">
       @foreach($following as $user)

+ 24 - 12
resources/views/profile/show.blade.php

@@ -4,19 +4,19 @@
 
 <div class="container">
   
-  <div class="profile-header row my-5 offset-md-1">
-    <div class="col-12 col-md-3">
-      <div class="profile-avatar">
+  <div class="profile-header row my-5">
+    <div class="col-12 col-md-4 d-flex">
+      <div class="profile-avatar mx-auto">
         <img class="img-thumbnail" src="{{$user->avatarUrl()}}" style="border-radius:100%;" width="172px">
       </div>
     </div>
-    <div class="col-12 col-md-9 d-flex align-items-center">
+    <div class="col-12 col-md-8 d-flex align-items-center">
       <div class="profile-details">
         <div class="username-bar pb-2  d-flex align-items-center">
-          <span class="font-weight-light h1">{{$user->username}}</span>
+          <span class="font-weight-ultralight h1">{{$user->username}}</span>
           @if($owner == true)
-          <span class="pl-4">
-            <a class="btn btn-outline-secondary font-weight-bold px-4 py-0" href="{{route('settings')}}">Settings</a>
+          <span class="h5 pl-2 b-0">
+            <a class="icon-settings text-muted" href="{{route('settings')}}"></a>
           </span>
           @elseif ($following == true)
           <span class="pl-4">
@@ -80,8 +80,8 @@
   <div class="profile-timeline mt-5 row">
     @if($owner == true)
       <div class="col-12 mb-5">
-        <ul class="nav nav-tabs d-flex justify-content-center">
-          <li class="nav-item mr-3">
+        <ul class="nav nav-topbar d-flex justify-content-center">
+          <li class="nav-item">
             <a class="nav-link {{request()->is('*/saved') ? '':'active'}} font-weight-bold text-uppercase" href="{{$user->url()}}">Posts</a>
           </li>
           <li class="nav-item">
@@ -100,8 +100,20 @@
     @if($timeline->count() > 0)
       @foreach($timeline as $status)
       <div class="col-12 col-md-4 mb-4">
-        <a class="card" href="{{$status->url()}}">
-          <img class="card-img-top" src="{{$status->thumb()}}" width="300px" height="300px">
+        <a class="card info-overlay" href="{{$status->url()}}">
+          <div class="square">
+            <div class="square-content" style="background-image: url('{{$status->thumb()}}')"></div>
+            <div class="info-overlay-text">
+              <h5 class="text-white m-auto font-weight-bold">
+                <span class="pr-4">
+                  <span class="icon-heart pr-1"></span> {{$status->likes_count}}
+                </span>
+                <span>
+                  <span class="icon-speech pr-1"></span> {{$status->comments_count}}
+                </span>
+              </h5>
+            </div>
+          </div>
         </a>
       </div>
       @endforeach
@@ -120,4 +132,4 @@
 
 </div>
 
-@endsection
+@endsection

+ 68 - 66
resources/views/status/show.blade.php

@@ -2,86 +2,88 @@
 
 @section('content')
 
-<div class="container">
-  <div class="col-12 mt-4">
-    
-    <div class="card status-container orientation-{{$status->firstMedia()->orientation ?? 'unknown'}}">
-      <div class="card-body p-0">
-        <div class="row">
-          <div class="col-12 col-md-8 status-photo">
-            <img src="{{$status->mediaUrl()}}" width="100%">
-          </div>
-          <div class="col-12 col-md-4" style="height:100%">
-            <div class="status-username d-inline-flex align-items-center pr-3 pt-3">
-              <div class="status-avatar mr-2">
-                <img class="img-thumbnail" src="{{$user->avatarUrl()}}" width="50px" height="50px" style="border-radius:40px;">
-              </div>
-              <div class="username">
-                <a href="{{$user->url()}}" class="username-link font-weight-bold text-dark">{{$user->username}}</a>
-              </div>
+<div class="container px-0 mt-md-4">
+  <div class="card status-container orientation-{{$status->firstMedia()->orientation ?? 'unknown'}}">
+    <div class="row mx-0">
+      <div class="col-12 col-md-8 status-photo px-0">
+        <img src="{{$status->mediaUrl()}}" width="100%">
+      </div>
+      <div class="col-12 col-md-4 px-0 d-flex flex-column">
+        <div class="d-flex align-items-center justify-content-between card-header">
+          <div class="d-flex align-items-center status-username">
+            <div class="status-avatar mr-2">
+              <img class="img-thumbnail" src="{{$user->avatarUrl()}}" width="24px" height="24px" style="border-radius:12px;">
             </div>
-            <hr>
-            <div class="pr-3 mb-2 status-comments">
-              <div class="status-comment">
-                <span class="font-weight-bold pr-1">{{$status->profile->username}}</span>
-                <p class="mb-1">
-                <span class="comment-text">{!! $status->rendered ?? e($status->caption) !!}</span>
-              </p>
-              <div class="comments">
+            <div class="username">
+              <a href="{{$user->url()}}" class="username-link font-weight-bold text-dark">{{$user->username}}</a>
+            </div>
+          </div>
+          <div class="timestamp mb-0">
+            <p class="small text-uppercase mb-0"><a href="{{$status->url()}}" class="text-muted">{{$status->created_at->diffForHumans(null, true, true, true)}}</a></p>
+          </div>
+        </div>
+        <div class="card-body status-comments">
+          <div class="status-comment">
+            <p class="mb-1">
+              <span class="font-weight-bold pr-1">{{$status->profile->username}}</span>
+              <span class="comment-text">{!! $status->rendered ?? e($status->caption) !!}</span>
+            </p>
+            <div class="comments">
               @foreach($status->comments->reverse()->take(10) as $item)
-                <p class="mb-0">
-                  <span class="font-weight-bold pr-1"><bdi><a class="text-dark" href="{{$item->profile->url()}}">{{$item->profile->username}}</a></bdi></span>
-                  <span class="comment-text">{!!$item->rendered!!} <a href="{{$item->url()}}" class="text-dark small font-weight-bold float-right">{{$item->created_at->diffForHumans(null, true, true ,true)}}</a></span>
-                </p>
+              <p class="mb-0">
+                <span class="font-weight-bold pr-1"><bdi><a class="text-dark" href="{{$item->profile->url()}}">{{$item->profile->username}}</a></bdi></span>
+                <span class="comment-text">{!!$item->rendered!!} <a href="{{$item->url()}}" class="text-dark small font-weight-bold float-right">{{$item->created_at->diffForHumans(null, true, true ,true)}}</a></span>
+              </p>
               @endforeach
-              </div>
-              </div>
             </div>
-            <div>
-            <div class="reactions h3 pr-3 mb-0">
-            <form class="like-form pr-3" method="post" action="/i/like" style="display: inline;" data-id="{{$status->id}}" data-action="like">
+          </div>
+        </div>
+        <div class="card-body flex-grow-0">
+          <div class="reactions h3 mb-0">
+            <form class="d-inline-flex like-form pr-3" method="post" action="/i/like" style="display: inline;" data-id="{{$status->id}}" data-action="like">
               @csrf
               <input type="hidden" name="item" value="{{$status->id}}">
-              <button class="btn btn-link text-dark p-0" type="submit"><span class="icon-heart" style="font-size:25px;"></span></button>
+              <button class="btn btn-link text-dark p-0" type="submit">
+                <span class="far fa-heart fa-lg mb-0"></span>
+              </button>
             </form>
-              <span class="icon-speech pr-3"></span>
-              @if(Auth::check())
-                @if(Auth::user()->profile->id === $status->profile->id || Auth::user()->is_admin == true)
-                <form method="post" action="/i/delete" class="d-inline-flex">
-                  @csrf
-                  <input type="hidden" name="type" value="post">
-                  <input type="hidden" name="item" value="{{$status->id}}">
-                  <button type="submit" class="btn btn-link text-dark p-0"><span class="icon-trash" style="font-size:25px;"></span></button>
-                </form>
-                @endif
-              @endif
-              <span class="float-right">
-                <form class="bookmark-form" method="post" action="/i/bookmark" style="display: inline;" data-id="{{$status->id}}" data-action="bookmark">
-                  @csrf
-                  <input type="hidden" name="item" value="{{$status->id}}">
-                  <button class="btn btn-link text-dark p-0" type="submit"><span class="icon-notebook" style="font-size:25px;"></span></button>
-                </form>
-              </span>
-            </div>
-            <div class="likes font-weight-bold mb-0">
-              <span class="like-count">{{$status->likes()->count()}}</span> likes
-            </div>
-            <div class="timestamp mb-0">
-              <p class="small text-uppercase mb-0"><a href="{{$status->url()}}" class="text-muted">{{$status->created_at->diffForHumans()}}</a></p>
-            </div>
-            <hr class="my-2">
-            <div class="pr-3 pb-2">
-              <form class="comment-form" method="post" action="/i/comment" data-id="{{$status->id}}" data-truncate="false">
+            <span class="far fa-comment fa-lg pt-1 pr-3"></span>
+            @if(Auth::check())
+            @if(Auth::user()->profile->id === $status->profile->id || Auth::user()->is_admin == true)
+            <form method="post" action="/i/delete" class="d-inline-flex">
+              @csrf
+              <input type="hidden" name="type" value="post">
+              <input type="hidden" name="item" value="{{$status->id}}">
+              <button type="submit" class="btn btn-link text-dark p-0">
+                <span class="far fa-trash-alt fa-lg mb-0"></span>
+              </button>
+            </form>
+            @endif
+            @endif
+            <span class="float-right">
+              <form class="d-inline-flex bookmark-form" method="post" action="/i/bookmark" style="display: inline;" data-id="{{$status->id}}" data-action="bookmark">
                 @csrf
                 <input type="hidden" name="item" value="{{$status->id}}">
-                <input class="form-control" name="comment" placeholder="Add a comment...">
+                <button class="btn btn-link text-dark p-0" type="submit">
+                  <span class="far fa-bookmark fa-lg mb-0"></span>
+                </button>
               </form>
-            </div>
+            </span>
           </div>
+          <div class="likes font-weight-bold mb-0">
+            <span class="like-count" data-count="{{$status->likes_count}}">{{$status->likes_count}}</span> likes
+          </div>
+        </div>
+        <div class="card-footer">
+          <form class="comment-form" method="post" action="/i/comment" data-id="{{$status->id}}" data-truncate="false">
+            @csrf
+            <input type="hidden" name="item" value="{{$status->id}}">
+            <input class="form-control" name="comment" placeholder="Add a comment...">
+          </form>
         </div>
       </div>
     </div>
   </div>
 </div>
 
-@endsection
+@endsection

+ 8 - 6
resources/views/status/template.blade.php

@@ -33,22 +33,24 @@
         </a>
         <div class="card-body">
           <div class="reactions h3">
-            <form class="like-form pr-3" method="post" action="/i/like" style="display: inline;" data-id="{{$item->id}}" data-action="like">
+            <form class="like-form pr-3" method="post" action="/i/like" style="display: inline;" data-id="{{$item->id}}" data-action="like" data-count="{{$item->likes_count}}">
               @csrf
               <input type="hidden" name="item" value="{{$item->id}}">
-              <button class="btn btn-link text-dark p-0" type="submit"><span class="icon-heart" style="font-size:25px;"></span></button>
+              <button class="btn btn-link text-dark p-0" type="submit">
+                <span class="far fa-heart status-heart fa-2x"></span>
+              </button>
             </form>
-            <span class="icon-speech"></span>
+            <span class="far fa-comment status-comment-focus"></span>
             <span class="float-right">
               <form class="bookmark-form" method="post" action="/i/bookmark" style="display: inline;" data-id="{{$item->id}}" data-action="bookmark">
                 @csrf
                 <input type="hidden" name="item" value="{{$item->id}}">
-                <button class="btn btn-link text-dark p-0" type="submit"><span class="icon-notebook" style="font-size:25px;"></span></button>
+                <button class="btn btn-link text-dark p-0" type="submit"><span class="far fa-bookmark" style="font-size:25px;"></span></button>
               </form>
             </span>
           </div>
           <div class="likes font-weight-bold">
-            <span class="like-count">{{$item->likes()->count()}}</span> likes
+            <span class="like-count">{{$item->likes_count}}</span> likes
           </div>
           <div class="caption">
             <p class="mb-1">
@@ -95,7 +97,7 @@
           <form class="comment-form" method="post" action="/i/comment" data-id="{{$item->id}}" data-truncate="true">
             @csrf
             <input type="hidden" name="item" value="{{$item->id}}">
-            <input class="form-control" name="comment" placeholder="Add a comment...">
+            <input class="form-control status-reply-input" name="comment" placeholder="Add a comment...">
           </form>
         </div>
       </div>

+ 23 - 0
resources/views/timeline/partial/new-form.blade.php

@@ -0,0 +1,23 @@
+    <div class="card">
+      <div class="card-header font-weight-bold">New Post</div>
+      <div class="card-body" id="statusForm">
+        <form method="post" action="/timeline" enctype="multipart/form-data">
+          @csrf
+          <div class="form-group">
+            <label class="font-weight-bold text-muted small">Upload Image</label>
+            <input type="file" class="form-control-file" name="photo" accept="image/*">
+            <small class="form-text text-muted">
+              Max Size: @maxFileSize(). Supported formats: jpeg, png, gif, bmp.
+            </small>
+          </div>
+          <div class="form-group">
+            <label class="font-weight-bold text-muted small">Caption</label>
+            <input type="text" class="form-control" name="caption" placeholder="Add a caption here">
+            <small class="form-text text-muted">
+              Max length: {{config('pixelfed.max_caption_length')}} characters.
+            </small>
+          </div>
+          <button type="submit" class="btn btn-outline-primary btn-block">Post</button>
+        </form>
+      </div>  
+    </div>

+ 2 - 17
resources/views/timeline/personal.blade.php

@@ -17,24 +17,9 @@
           </ul>
       </div>
     @endif
-    <div class="card">
-      <div class="card-header font-weight-bold">New Post</div>
-      <div class="card-body" id="statusForm">
-        <form method="post" action="/timeline" enctype="multipart/form-data">
-          @csrf
-          <div class="form-group">
-            <label class="font-weight-bold text-muted small">Upload Image</label>
-            <input type="file" class="form-control-file" name="photo" accept="image/*">
-          </div>
-          <div class="form-group">
-            <label class="font-weight-bold text-muted small">Caption</label>
-            <input type="text" class="form-control" name="caption" placeholder="Add a caption here">
-          </div>
-          <button type="submit" class="btn btn-outline-primary btn-block">Post</button>
-        </form>
-      </div>  
-    </div>
 
+    @include('timeline.partial.new-form')
+    
     <div class="timeline-feed my-5" data-timeline="personal">
     @foreach($timeline as $item)
 

+ 2 - 17
resources/views/timeline/public.blade.php

@@ -17,23 +17,8 @@
           </ul>
       </div>
     @endif
-    <div class="card">
-      <div class="card-header font-weight-bold">New Post</div>
-      <div class="card-body" id="statusForm">
-        <form method="post" action="/timeline" enctype="multipart/form-data">
-          @csrf
-          <div class="form-group">
-            <label class="font-weight-bold text-muted small">Upload Image</label>
-            <input type="file" class="form-control-file" name="photo" accept="image/*">
-          </div>
-          <div class="form-group">
-            <label class="font-weight-bold text-muted small">Caption</label>
-            <input type="text" class="form-control" name="caption" placeholder="Add a caption here">
-          </div>
-          <button type="submit" class="btn btn-outline-primary btn-block">Post</button>
-        </form>
-      </div>  
-    </div>
+    
+    @include('timeline.partial.new-form')
 
     <div class="timeline-feed my-5" data-timeline="public">
     @foreach($timeline as $item)

+ 2 - 0
routes/web.php

@@ -47,6 +47,7 @@ Route::domain(config('pixelfed.domain.app'))->group(function() {
     Route::get('search/{tag}', 'SearchController@searchAPI')
           ->where('tag', '[A-Za-z0-9]+');
     Route::get('nodeinfo/2.0.json', 'FederationController@nodeinfo');
+    Route::get('v1/likes', 'ApiController@hydrateLikes');
   });
 
   Route::get('discover/tags/{hashtag}', 'DiscoverController@showTags');
@@ -124,6 +125,7 @@ Route::domain(config('pixelfed.domain.app'))->group(function() {
     Route::view('libraries', 'site.libraries')->name('site.libraries');
   });
 
+  Route::get('p/{username}/{id}/c/{cid}', 'CommentController@show');
   Route::get('p/{username}/{id}', 'StatusController@show');
   Route::get('{username}/saved', 'ProfileController@savedBookmarks');
   Route::get('{username}/followers', 'ProfileController@followers');

Некоторые файлы не были показаны из-за большого количества измененных файлов