Browse Source

Merge pull request #410 from pixelfed/frontend-ui-refactor

Frontend ui refactor
daniel 6 years ago
parent
commit
f1e2ad2d34
56 changed files with 1765 additions and 136 deletions
  1. 1 1
      .dockerignore
  2. 0 31
      Dockerfile
  3. 1 0
      Dockerfile
  4. 1 1
      app/Http/Controllers/Auth/RegisterController.php
  5. 2 2
      app/Http/Controllers/SettingsController.php
  6. 76 11
      app/Http/Controllers/StatusController.php
  7. 11 0
      app/Http/Controllers/StoryController.php
  8. 19 3
      app/Http/Controllers/TimelineController.php
  9. 8 2
      app/Jobs/ImageOptimizePipeline/ImageUpdate.php
  10. 4 1
      app/Jobs/RemoteFollowPipeline/RemoteFollowImportRecent.php
  11. 6 1
      app/Profile.php
  12. 2 0
      app/Report.php
  13. 94 0
      app/Util/ActivityPub/Concern/HTTPSignature.php
  14. 21 1
      config/pixelfed.php
  15. 27 0
      config/trustedproxy.php
  16. 59 0
      contrib/docker/Dockerfile.apache
  17. 31 0
      contrib/docker/Dockerfile.fpm
  18. 5 0
      contrib/docker/php.ini
  19. 17 0
      contrib/docker/start.sh
  20. 6 4
      database/migrations/2018_08_22_022306_update_settings_table.php
  21. 44 0
      database/migrations/2018_08_27_004653_update_media_table_add_alt_text.php
  22. 41 33
      docker-compose.yml
  23. 10 0
      resources/assets/sass/custom.scss
  24. 3 0
      resources/lang/en/profile.php
  25. 6 1
      resources/views/account/verify_email.blade.php
  26. 57 0
      resources/views/admin/reports/home.blade.php
  27. 167 0
      resources/views/admin/reports/show.blade.php
  28. 74 0
      resources/views/admin/settings/backups.blade.php
  29. 9 0
      resources/views/admin/settings/maintenance.blade.php
  30. 30 0
      resources/views/admin/settings/storage.blade.php
  31. 39 0
      resources/views/admin/settings/system.blade.php
  32. 40 0
      resources/views/layouts/anon.blade.php
  33. 8 0
      resources/views/layouts/partial/noauthnav.blade.php
  34. 32 0
      resources/views/profile/partial/private-info.blade.php
  35. 4 0
      resources/views/profile/partial/user-info.blade.php
  36. 33 0
      resources/views/profile/private.blade.php
  37. 1 0
      resources/views/profile/show.blade.php
  38. 45 0
      resources/views/report/abusive/comment.blade.php
  39. 45 0
      resources/views/report/abusive/post.blade.php
  40. 45 0
      resources/views/report/abusive/profile.blade.php
  41. 70 21
      resources/views/report/form.blade.php
  42. 5 0
      resources/views/report/not-interested.blade.php
  43. 45 0
      resources/views/report/sensitive/comment.blade.php
  44. 45 0
      resources/views/report/sensitive/post.blade.php
  45. 45 0
      resources/views/report/sensitive/profile.blade.php
  46. 12 3
      resources/views/report/spam.blade.php
  47. 21 18
      resources/views/report/spam/post.blade.php
  48. 1 1
      resources/views/report/spam/profile.blade.php
  49. 4 1
      resources/views/settings/home.blade.php
  50. 91 0
      resources/views/status/edit.blade.php
  51. 65 0
      resources/views/status/show/album.blade.php
  52. 46 0
      resources/views/status/show/photo.blade.php
  53. 101 0
      resources/views/status/show/sidebar.blade.php
  54. 50 0
      resources/views/status/show/video.blade.php
  55. 9 0
      routes/web.php
  56. 31 0
      tests/Unit/CryptoTest.php

+ 1 - 1
.dockerignore

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

+ 0 - 31
Dockerfile

@@ -1,31 +0,0 @@
-FROM php:7.2.6-fpm-alpine
-
-ARG COMPOSER_VERSION="1.6.5"
-ARG COMPOSER_CHECKSUM="67bebe9df9866a795078bb2cf21798d8b0214f2e0b2fd81f2e907a8ef0be3434"
-
-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 bcmath && \
-  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
-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}"

+ 1 - 0
Dockerfile

@@ -0,0 +1 @@
+contrib/docker/Dockerfile.apache

+ 1 - 1
app/Http/Controllers/Auth/RegisterController.php

@@ -65,7 +65,7 @@ class RegisterController extends Controller
         ];        
 
         $rules = [
-            'name' => 'required|string|max:255',
+            'name' => 'required|string|max' . config('pixelfed.max_name_length'),
             'username' => $usernameRules,
             'email' => 'required|string|email|max:255|unique:users',
             'password' => 'required|string|min:6|confirmed',

+ 2 - 2
app/Http/Controllers/SettingsController.php

@@ -30,8 +30,8 @@ class SettingsController extends Controller
     public function homeUpdate(Request $request)
     {
       $this->validate($request, [
-        'name'  => 'required|string|max:30',
-        'bio'   => 'nullable|string|max:125',
+        'name'  => 'required|string|max:' . config('pixelfed.max_name_length'),
+        'bio'   => 'nullable|string|max:' . config('pixelfed.max_bio_length')
         'website' => 'nullable|url',
         'email' => 'nullable|email'
       ]);

+ 76 - 11
app/Http/Controllers/StatusController.php

@@ -21,17 +21,33 @@ class StatusController extends Controller
                 ->withCount(['likes', 'comments', 'media'])
                 ->findOrFail($id);
 
-        if(!$status->media_path && $status->in_reply_to_id) {
-          return redirect($status->url());
-        }
-
         if($request->wantsJson() && config('pixelfed.activitypub_enabled')) {
           return $this->showActivityPub($request, $status);
         }
 
+        $template = $this->detectTemplate($status);
+
         $replies = Status::whereInReplyToId($status->id)->simplePaginate(30);
 
-        return view('status.show', compact('user', 'status', 'replies'));
+        return view($template, compact('user', 'status', 'replies'));
+    }
+
+    protected function detectTemplate($status)
+    {
+        $template = Cache::rememberForever('template:status:type:'.$status->id, function () use($status) {
+          $template = 'status.show.photo';
+          if(!$status->media_path && $status->in_reply_to_id) {
+            $template = 'status.reply';
+          }
+          if($status->media->count() > 1) {
+            $template = 'status.show.album';
+          }
+          if($status->viewType() == 'video') {
+            $template = 'status.show.video';
+          }
+          return $template;
+        });
+        return $template;
     }
 
     public function compose()
@@ -42,11 +58,7 @@ class StatusController extends Controller
 
     public function store(Request $request)
     {
-        if(Auth::check() == false)
-        { 
-          abort(403); 
-        }
-
+        $this->authCheck();
         $user = Auth::user();
 
         $size = Media::whereUserId($user->id)->sum('size') / 1000;
@@ -56,7 +68,7 @@ class StatusController extends Controller
         }
 
         $this->validate($request, [
-          'photo.*'   => 'required|mimes:jpeg,png,bmp,gif|max:' . config('pixelfed.max_photo_size'),
+          'photo.*'   => 'required|mimes:jpeg,png,gif|max:' . config('pixelfed.max_photo_size'),
           'caption' => 'string|max:' . config('pixelfed.max_caption_length'),
           'cw'      => 'nullable|string',
           'filter_class' => 'nullable|string',
@@ -83,11 +95,13 @@ class StatusController extends Controller
         foreach ($photos as $k => $v) {
           $storagePath = "public/m/{$monthHash}/{$userHash}";
           $path = $v->store($storagePath);
+          $hash = \hash_file('sha256', $v);
           $media = new Media;
           $media->status_id = $status->id;
           $media->profile_id = $profile->id;
           $media->user_id = $user->id;
           $media->media_path = $path;
+          $media->original_sha256 = $hash;
           $media->size = $v->getClientSize();
           $media->mime = $v->getClientMimeType();
           $media->filter_class = $request->input('filter_class');
@@ -172,6 +186,57 @@ class StatusController extends Controller
       return response(json_encode($res['data']))->header('Content-Type', 'application/activity+json');
     }
 
+    public function edit(Request $request, $username, $id)
+    {
+        $this->authCheck();
+        $user = Auth::user()->profile;
+        $status = Status::whereProfileId($user->id)
+                ->with(['media'])
+                ->findOrFail($id);
+        return view('status.edit', compact('user', 'status'));
+    }
+
+
+    public function editStore(Request $request, $username, $id)
+    {
+        $this->authCheck();
+        $user = Auth::user()->profile;
+        $status = Status::whereProfileId($user->id)
+                ->with(['media'])
+                ->findOrFail($id);
+
+        $this->validate($request, [
+          'id' => 'required|integer|min:1',
+          'caption' => 'nullable',
+          'filter' => 'nullable|alpha_dash|max:30'
+        ]);
+
+        $id = $request->input('id');
+        $caption = $request->input('caption');
+        $filter = $request->input('filter');
+
+        $media = Media::whereProfileId($user->id)
+            ->whereStatusId($status->id)
+            ->find($id);
+
+        $changed = false;
+
+        if($media->caption != $caption) {
+          $media->caption = $caption;
+          $changed = true;
+        }
+
+        if($media->filter_class != $filter) {
+          $media->filter_class = $filter;
+          $changed = true;
+        }
+
+        if($changed === true) {
+          $media->save();
+        }
+        return response()->json([], 200);
+    }
+
     protected function authCheck()
     {
         if(Auth::check() == false)

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

@@ -0,0 +1,11 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use Illuminate\Http\Request;
+use App\Profile;
+
+class StoryController extends Controller
+{
+
+}

+ 19 - 3
app/Http/Controllers/TimelineController.php

@@ -4,7 +4,7 @@ namespace App\Http\Controllers;
 
 use Illuminate\Http\Request;
 use Auth;
-use App\{Follower, Status, User};
+use App\{Follower, Profile, Status, User, UserFilter};
 
 class TimelineController extends Controller
 {
@@ -15,10 +15,16 @@ class TimelineController extends Controller
 
     public function personal()
     {
+      $pid = Auth::user()->profile->id;
       // TODO: Use redis for timelines
-      $following = Follower::whereProfileId(Auth::user()->profile->id)->pluck('following_id');
-      $following->push(Auth::user()->profile->id);
+      $following = Follower::whereProfileId($pid)->pluck('following_id');
+      $following->push($pid);
+      $filtered = UserFilter::whereUserId($pid)
+                  ->whereFilterableType('App\Profile')
+                  ->whereIn('filter_type', ['mute', 'block'])
+                  ->pluck('filterable_id');
       $timeline = Status::whereIn('profile_id', $following)
+                  ->whereNotIn('profile_id', $filtered)
                   ->orderBy('id','desc')
                   ->withCount(['comments', 'likes'])
                   ->simplePaginate(20);
@@ -30,8 +36,18 @@ class TimelineController extends Controller
     {
       // TODO: Use redis for timelines
       // $timeline = Timeline::build()->local();
+      $pid = Auth::user()->profile->id;
+
+      $filtered = UserFilter::whereUserId($pid)
+                  ->whereFilterableType('App\Profile')
+                  ->whereIn('filter_type', ['mute', 'block'])
+                  ->pluck('filterable_id');
+      $private = Profile::whereIsPrivate(true)->pluck('id');
+      $filtered = $filtered->merge($private);
       $timeline = Status::whereHas('media')
+                  ->whereNotIn('profile_id', $filtered)
                   ->whereNull('in_reply_to_id')
+                  ->whereNull('reblog_of_id')
                   ->withCount(['comments', 'likes'])
                   ->orderBy('id','desc')
                   ->simplePaginate(20);

+ 8 - 2
app/Jobs/ImageOptimizePipeline/ImageUpdate.php

@@ -16,6 +16,12 @@ class ImageUpdate implements ShouldQueue
 
     protected $media;
 
+    protected $protectedMimes = [
+        'image/gif',
+        'image/bmp',
+        'video/mp4'
+    ];
+
     /**
      * Create a new job instance.
      *
@@ -37,9 +43,9 @@ class ImageUpdate implements ShouldQueue
         $path = storage_path('app/'. $media->media_path);
         $thumb = storage_path('app/'. $media->thumbnail_path);
         try {
-            ImageOptimizer::optimize($thumb);
-            if($media->mime !== 'image/gif')
+            if(!in_array($media->mime, $this->protectedMimes))
             {
+                ImageOptimizer::optimize($thumb);
                 ImageOptimizer::optimize($path);
             }
         } catch (Exception $e) {

+ 4 - 1
app/Jobs/RemoteFollowPipeline/RemoteFollowImportRecent.php

@@ -13,6 +13,7 @@ use Illuminate\Queue\InteractsWithQueue;
 use Illuminate\Contracts\Queue\ShouldQueue;
 use Illuminate\Foundation\Bus\Dispatchable;
 use App\Jobs\StatusPipeline\NewStatusPipeline;
+use App\Jobs\ImageOptimizePipeline\ImageThumbnail;
 
 class RemoteFollowImportRecent implements ShouldQueue
 {
@@ -216,7 +217,9 @@ class RemoteFollowImportRecent implements ShouldQueue
           $media->size = 0;
           $media->mime = $mime;
           $media->save();
-
+          
+          ImageThumbnail::dispatch($media);
+          
           return true;
       } catch (Exception $e) {
           return false;

+ 6 - 1
app/Profile.php

@@ -143,7 +143,11 @@ class Profile extends Model
 
     public function statusCount()
     {
-        return $this->statuses()->whereHas('media')->count();
+        return $this->statuses()
+        ->whereHas('media')
+        ->whereNull('in_reply_to_id')
+        ->whereNull('reblog_of_id')
+        ->count();
     }
 
     public function recommendFollowers()
@@ -159,6 +163,7 @@ class Profile extends Model
             ->whereNotIn('following_id', $follows)
             ->whereIn('profile_id', $following)
             ->orderByRaw('rand()')
+            ->distinct('id')
             ->limit(3)
             ->pluck('following_id');
         $recommended = [];

+ 2 - 0
app/Report.php

@@ -6,6 +6,8 @@ use Illuminate\Database\Eloquent\Model;
 
 class Report extends Model
 {
+    protected $dates = ['admin_seen'];
+    
     public function url()
     {
       return url('/i/admin/reports/show/' . $this->id);

+ 94 - 0
app/Util/ActivityPub/Concern/HTTPSignature.php

@@ -0,0 +1,94 @@
+<?php
+
+namespace App\Util\ActivityPub\Concern;
+
+use Zttp\Zttp;
+
+class HTTPSignature {
+
+    protected $localhosts = [
+      '127.0.0.1', 'localhost', '::1'
+    ];
+    public $profile;
+    public $is_url;
+
+    public function validateUrl()
+    {
+        // If the profile exists, assume its valid
+        if($this->is_url === false) {
+          return true;
+        }
+
+        $url = $this->profile;
+        try {
+          $url = filter_var($url, FILTER_VALIDATE_URL);
+          $parsed = parse_url($url, PHP_URL_HOST);
+          if(!$parsed || in_array($parsed, $this->localhosts)) {
+            return false;
+          }
+        } catch (Exception $e) {
+          return false;
+        }
+        return true;
+    }
+
+    public function fetchPublicKey($profile, bool $is_url = true)
+    {
+        $this->profile = $profile;
+        $this->is_url = $is_url;
+        $valid = $this->validateUrl();
+        if(!$valid) {
+          throw new \Exception('Invalid URL provided');
+        }
+        if($is_url && isset($profile->public_key) && $profile->public_key) {
+          return $profile->public_key;
+        }
+
+        try {
+          $url = $this->profile;
+          $res = Zttp::timeout(30)->withHeaders([
+              'Accept' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
+              'User-Agent' => 'PixelFedBot v0.1 - https://pixelfed.org'
+            ])->get($url);
+          $actor = json_decode($res->getBody(), true);
+        } catch (Exception $e) {
+          throw new Exception('Unable to fetch public key');
+        }
+
+        return $actor['publicKey']['publicKeyPem'];
+    }
+
+    public function sendSignedObject($senderProfile, $url, $body)
+    {
+        $profile = $senderProfile;
+        $context = new Context([
+            'keys' => [$profile->keyId() => $profile->private_key],
+            'algorithm' => 'rsa-sha256',
+            'headers' => ['(request-target)', 'Date'],
+        ]);
+
+        $handlerStack = GuzzleHttpSignatures::defaultHandlerFromContext($context);
+        $client = new Client(['handler' => $handlerStack]);
+
+        $headers = [
+            'Accept' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
+            'Date' => date('D, d M Y h:i:s') . ' GMT',
+            'Content-Type' => 'application/activity+json',
+            'User-Agent' => 'PixelFedBot - https://pixelfed.org'
+        ];
+        
+        $response = $client->post($url, [
+            'options' => [
+                'allow_redirects' => false,
+                'verify' => true,
+                'timeout' => 30
+            ],
+            'headers' => $headers, 
+            'body' => $body
+        ]);
+
+        return $response->getBody();
+    }
+
+
+}

+ 21 - 1
config/pixelfed.php

@@ -108,6 +108,26 @@ return [
     */
     'max_caption_length' => env('MAX_CAPTION_LENGTH', 500),
 
+    /*
+    |--------------------------------------------------------------------------
+    | Bio length limit
+    |--------------------------------------------------------------------------
+    |
+    | Change the bio length limit for user profiles.
+    |
+    */
+    'max_bio_length' => env('MAX_BIO_LENGTH', 125),
+
+    /*
+    |--------------------------------------------------------------------------
+    | User name length limit
+    |--------------------------------------------------------------------------
+    |
+    | Change the length limit for user names.
+    |
+    */
+    'max_name_length' => env('MAX_NAME_LENGTH', 30),
+
     /*
     |--------------------------------------------------------------------------
     | Album size limit
@@ -138,4 +158,4 @@ return [
     */
     'image_quality'  => (int) env('IMAGE_QUALITY', 80),
     
-];
+];

+ 27 - 0
config/trustedproxy.php

@@ -0,0 +1,27 @@
+<?php
+
+return [
+    /*
+     * Set trusted proxy IP addresses.
+     *
+     * Both IPv4 and IPv6 addresses are
+     * supported, along with CIDR notation.
+     *
+     * The "*" character is syntactic sugar
+     * within TrustedProxy to trust any proxy
+     * that connects directly to your server,
+     * a requirement when you cannot know the address
+     * of your proxy (e.g. if using Rackspace balancers).
+     *
+     * The "**" character is syntactic sugar within
+     * TrustedProxy to trust not just any proxy that
+     * connects directly to your server, but also
+     * proxies that connect to those proxies, and all
+     * the way back until you reach the original source
+     * IP. It will mean that $request->getClientIp()
+     * always gets the originating client IP, no matter
+     * how many proxies that client's request has
+     * subsequently passed through.
+     */
+    'proxies' => explode(',', env('TRUST_PROXIES', '')),
+];

+ 59 - 0
contrib/docker/Dockerfile.apache

@@ -0,0 +1,59 @@
+FROM php:7-apache
+
+ARG COMPOSER_VERSION="1.6.5"
+ARG COMPOSER_CHECKSUM="67bebe9df9866a795078bb2cf21798d8b0214f2e0b2fd81f2e907a8ef0be3434"
+
+RUN apt-get update \
+ && apt-get install -y --no-install-recommends git \
+      optipng pngquant jpegoptim gifsicle \
+      libfreetype6 libjpeg62-turbo libpng16-16 libxpm4 libvpx4 libmagickwand-6.q16-3 \
+      libfreetype6-dev libjpeg62-turbo-dev libpng-dev libxpm-dev libvpx-dev libmagickwand-dev \
+ && docker-php-source extract \
+ && docker-php-ext-configure gd \
+      --with-freetype-dir=/usr/lib/x86_64-linux-gnu/ \
+      --with-jpeg-dir=/usr/lib/x86_64-linux-gnu/ \
+      --with-xpm-dir=/usr/lib/x86_64-linux-gnu/ \
+      --with-vpx-dir=/usr/lib/x86_64-linux-gnu/ \
+ && docker-php-ext-install pdo_mysql pcntl gd exif bcmath \
+ && pecl install imagick \
+ && docker-php-ext-enable imagick pcntl imagick gd exif \
+ && a2enmod rewrite \
+ && curl -LsS https://getcomposer.org/download/${COMPOSER_VERSION}/composer.phar -o /usr/bin/composer \
+ && echo "${COMPOSER_CHECKSUM}  /usr/bin/composer" | sha256sum -c - \
+ && chmod 755 /usr/bin/composer \
+ && apt-get autoremove --purge -y \
+       libfreetype6-dev libjpeg62-turbo-dev libpng-dev libxpm-dev libvpx-dev libmagickwand-dev \
+ && rm -rf /var/cache/apt \
+ && docker-php-source delete
+
+ENV PATH="~/.composer/vendor/bin:./vendor/bin:${PATH}"
+
+COPY . /var/www/
+
+WORKDIR /var/www/
+RUN cp -r storage storage.skel \
+ && cp contrib/docker/php.ini /usr/local/etc/php/conf.d/pixelfed.ini \
+ && composer install --prefer-source --no-interaction \
+ && rm -rf html && ln -s public html
+
+VOLUME ["/var/www/storage"]
+
+ENV APP_ENV=production \
+    APP_DEBUG=false \
+    LOG_CHANNEL=stderr \
+    DB_CONNECTION=mysql \
+    DB_PORT=3306 \
+    DB_HOST=db \
+    BROADCAST_DRIVER=log \
+    QUEUE_DRIVER=redis \
+    HORIZON_PREFIX=horizon-pixelfed \
+    REDIS_HOST=redis \
+    SESSION_SECURE_COOKIE=true \
+    API_BASE="/api/1/" \
+    API_SEARCH="/api/search" \
+    OPEN_REGISTRATION=true \
+    ENFORCE_EMAIL_VERIFICATION=true \
+    REMOTE_FOLLOW=false \
+    ACTIVITY_PUB=false
+
+CMD /var/www/contrib/docker/start.sh

+ 31 - 0
contrib/docker/Dockerfile.fpm

@@ -0,0 +1,31 @@
+FROM php:7.2.6-fpm-alpine
+
+ARG COMPOSER_VERSION="1.6.5"
+ARG COMPOSER_CHECKSUM="67bebe9df9866a795078bb2cf21798d8b0214f2e0b2fd81f2e907a8ef0be3434"
+
+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
+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}"

+ 5 - 0
contrib/docker/php.ini

@@ -0,0 +1,5 @@
+file_uploads = On
+memory_limit = 64M
+upload_max_filesize = 64M
+post_max_size = 64M
+max_execution_time = 600

+ 17 - 0
contrib/docker/start.sh

@@ -0,0 +1,17 @@
+#!/bin/bash
+
+# Create the storage tree if needed and fix permissions
+cp -r storage.skel/* storage/
+chown -R www-data:www-data storage/
+php artisan storage:link
+
+# Migrate database if the app was upgraded
+php artisan migrate --force
+
+# Run a worker if it is set as embedded
+if [ HORIZON_EMBED = true ]; then
+	php artisan horizon &
+fi
+
+# Finally run Apache
+exec apache2-foreground

+ 6 - 4
database/migrations/2018_08_22_022306_update_settings_table.php

@@ -28,9 +28,11 @@ class UpdateSettingsTable extends Migration
      */
     public function down()
     {
-        $table->dropColumn('show_profile_followers');
-        $table->dropColumn('show_profile_follower_count');
-        $table->dropColumn('show_profile_following');
-        $table->dropColumn('show_profile_following_count');
+       Schema::table('user_settings', function (Blueprint $table) {
+            $table->dropColumn('show_profile_followers');
+            $table->dropColumn('show_profile_follower_count');
+            $table->dropColumn('show_profile_following');
+            $table->dropColumn('show_profile_following_count');
+        });
     }
 }

+ 44 - 0
database/migrations/2018_08_27_004653_update_media_table_add_alt_text.php

@@ -0,0 +1,44 @@
+<?php
+
+use Illuminate\Support\Facades\Schema;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Database\Migrations\Migration;
+
+class UpdateMediaTableAddAltText extends Migration
+{
+    /**
+     * Run the migrations.
+     *
+     * @return void
+     */
+    public function up()
+    {
+       Schema::table('media', function (Blueprint $table) {
+            $table->string('original_sha256')->nullable()->index()->after('user_id');
+            $table->string('optimized_sha256')->nullable()->index()->after('original_sha256');
+            $table->string('caption')->nullable()->after('thumbnail_url');
+            $table->string('hls_path')->nullable()->after('caption');
+            $table->timestamp('hls_transcoded_at')->nullable()->after('processed_at');
+            $table->string('key')->nullable();
+            $table->json('metadata')->nullable();
+       });
+    }
+
+    /**
+     * Reverse the migrations.
+     *
+     * @return void
+     */
+    public function down()
+    {
+       Schema::table('media', function (Blueprint $table) {
+            $table->dropColumn('original_sha256');
+            $table->dropColumn('optimized_sha256');
+            $table->dropColumn('caption');
+            $table->dropColumn('hls_path');
+            $table->dropColumn('hls_transcoded_at');
+            $table->dropColumn('key');
+            $table->dropColumn('metadata');
+       });
+    }
+}

+ 41 - 33
docker-compose.yml

@@ -1,49 +1,56 @@
 ---
 version: '3'
+
+# In order to set configuration, please use a .env file in
+# your compose project directory (the same directory as your
+# docker-compose.yml), and set database options, application
+# name, key, and other settings there.
+# A list of available settings is available in .env.example
+#
+# The services should scale properly across a swarm cluster
+# if the volumes are properly shared between cluster members.
+
 services:
-  nginx:
-    image: nginx:alpine
-    networks:
-      - internal
-      - external
-    ports:
-      - 3000:80
-    volumes:
-      - "php-storage:/var/www/html"
-      - ./contrib/nginx.conf:/etc/nginx/conf.d/default.conf
-    depends_on:
-      - php
 
-  php:
-    build: .
+  app:
+    # Uncomment to build a local copy of the image
+    # build: .
     image: pixelfed
+    # If you have a traefik running, uncomment this to expose Pixelfed
+    # labels:
+    #   - traefik.enable=true
+    #   - traefik.frontend.rule=Host:your.url
+    #   - traefik.port=80
+    env_file:
+      - ./.env
     volumes:
-      - "php-storage:/var/www/html"
+      - "app-storage:/var/www/storage"
     networks:
+      - external
       - internal
-    environment:
-      - DB_HOST=mysql
-      - DB_DATABASE=pixelfed
-      - DB_USERNAME=${DB_USERNAME:-pixelfed}
-      - DB_PASSWORD=${DB_PASSWORD:-pixelfed}
-      - REDIS_HOST=redis
-      - APP_KEY=${APP_KEY}
-    env_file:
-      - ./.env
 
-  mysql:
+  # Uncomment if you set HORIZON_EMBED to false and wish to run a local worker
+  # worker:
+  #   image: pixelfed
+  #   env_file:
+  #     - ./.env
+  #   volumes:
+  #     - "app-storage:/var/www/storage"
+  #   networks:
+  #     - internal
+  #   command: php artisan horizon
+    
+  db:
     image: mysql:5.7
     networks:
       - internal
     environment:
       - MYSQL_DATABASE=pixelfed
-      - MYSQL_USER=${DB_USERNAME:-pixelfed}
-      - MYSQL_PASSWORD=${DB_PASSWORD:-pixelfed}
-      - MYSQL_RANDOM_ROOT_PASSWORD="true"
-    env_file:
-      - ./.env
+      - MYSQL_USER=${DB_USERNAME}
+      - MYSQL_PASSWORD=${DB_PASSWORD}
+      - MYSQL_RANDOM_ROOT_PASSWORD=true
     volumes:
-      - "mysql-data:/var/lib/mysql"
+      - "db-data:/var/lib/mysql"
 
   redis:
     image: redis:4-alpine
@@ -52,10 +59,11 @@ services:
     networks:
       - internal
 
+# Adjust your volume data in order to store data where you wish
 volumes:
   redis-data:
-  mysql-data:
-  php-storage:
+  db-data:
+  app-storage:
 
 networks:
   internal:

+ 10 - 0
resources/assets/sass/custom.scss

@@ -298,3 +298,13 @@ details summary::-webkit-details-marker {
 .profile-avatar img {
   object-fit: cover;
 }
+
+.tt-menu {
+  padding: 0 !important;
+  border-radius: 0 0 0.25rem 0.25rem !important;
+}
+
+.tt-dataset .alert {
+  border: 0 !important;
+  border-radius: 0 !important;
+}

+ 3 - 0
resources/lang/en/profile.php

@@ -6,4 +6,7 @@ return [
   'emptyFollowing' => 'This user is not following anyone yet!',
   'emptySaved' => 'You haven’t saved any post yet!',
   'savedWarning'  => 'Only you can see what you’ve saved',
+  'privateProfileWarning' => 'This Account is Private',
+  'alreadyFollow' => 'Already follow :username?',
+  'loginToSeeProfile' => 'to see their photos and videos.',
 ];

+ 6 - 1
resources/views/account/verify_email.blade.php

@@ -5,7 +5,12 @@
   <div class="col-12 col-md-8 offset-md-2">
     @if (session('status'))
         <div class="alert alert-success">
-            {{ session('status') }}
+            <p class="font-weight-bold mb-0">{{ session('status') }}</p>
+        </div>
+    @endif
+    @if (session('error'))
+        <div class="alert alert-danger">
+            <p class="font-weight-bold mb-0">{{ session('error') }}</p>
         </div>
     @endif
     <div class="card">

+ 57 - 0
resources/views/admin/reports/home.blade.php

@@ -0,0 +1,57 @@
+@extends('admin.partial.template')
+
+@section('section')
+  <div class="title">
+    <h3 class="font-weight-bold">Reports</h3>
+  </div>
+
+  <hr>
+
+  <table class="table">
+    <thead class="thead-dark">
+      <tr>
+        <th scope="col">#</th>
+        <th scope="col">Reporter</th>
+        <th scope="col">Type</th>
+        <th scope="col">Reported</th>
+        <th scope="col">Status</th>
+        <th scope="col">Created</th>
+      </tr>
+    </thead>
+    <tbody>
+      @foreach($reports as $report)
+      <tr>
+        <th scope="row">
+          <a href="{{$report->url()}}">
+            {{$report->id}}
+          </a>
+        </th>
+        <td class="font-weight-bold"><a href="{{$report->reporter->url()}}">{{$report->reporter->username}}</a></td>
+        <td class="font-weight-bold">{{$report->type}}</td>
+        <td class="font-weight-bold"><a href="{{$report->reported()->url()}}">{{str_limit($report->reported()->url(), 25)}}</a></td>
+        @if(!$report->admin_seen)
+        <td><span class="text-danger font-weight-bold">Unresolved</span></td>
+        @else
+        <td><span class="text-success font-weight-bold">Resolved</span></td>
+        @endif
+        <td class="font-weight-bold">{{$report->created_at->diffForHumans(null, true, true, true)}}</td>
+      </tr>
+      @endforeach
+    </tbody>
+  </table>
+  <div class="d-flex justify-content-center mt-5 small">
+    {{$reports->links()}}
+  </div>
+@endsection
+
+@push('scripts')
+  <script type="text/javascript">
+    $(document).ready(function() {
+      $('.human-size').each(function(d,a) {
+        let el = $(a);
+        let size = el.data('bytes');
+        el.text(filesize(size, {round: 0}));
+      });
+    });
+  </script>
+@endpush

+ 167 - 0
resources/views/admin/reports/show.blade.php

@@ -0,0 +1,167 @@
+@extends('admin.partial.template')
+
+@section('section')
+  <div class="title">
+    <h3 class="font-weight-bold">Report #<span class="reportid" data-id="{{$report->id}}">{{$report->id}}</span> - <span class="badge badge-danger">{{ucfirst($report->type)}}</span></h3>
+  </div>
+
+  <div class="card">
+    <div class="card-body">
+      <h5 class="card-title">Reported: <a href="{{$report->reported()->url()}}">{{$report->reported()->url()}}</a></h5>
+      <h6 class="card-subtitle mb-2 text-muted">Reported by: <a href="{{$report->reporter->url()}}">{{$report->reporter->username}}</a> <span class="badge badge-primary">admin</span></h6>
+      <p class="card-text text-muted">
+        <span class="font-weight-bold text-dark">Message: </span>
+        {{$report->message ?? 'No message provided.'}}
+      </p>
+
+      @if(!$report->admin_seen)
+      <a href="#" class="card-link report-action-btn font-weight-bold" data-action="ignore">Ignore</a>
+      {{-- <a href="#" class="card-link font-weight-bold">Request Mod Feedback</a> --}}
+      <a href="#" class="card-link report-action-btn font-weight-bold" data-action="cw">Add CW</a>
+      <a href="#" class="card-link report-action-btn font-weight-bold" data-action="unlist">Unlist/Hide</a>
+      <a href="#" class="card-link report-action-btn font-weight-bold text-danger" data-action="delete">Delete</a>
+      <a href="#" class="card-link report-action-btn font-weight-bold text-danger" data-action="shadowban">Shadowban User</a>
+      <a href="#" class="card-link report-action-btn font-weight-bold text-danger" data-action="ban">Ban User</a>
+      @else
+      <p class="font-weight-bold mb-0">Resolved {{$report->admin_seen->diffForHumans()}}</p>
+      @endif
+    </div>
+  </div>
+
+  <div class="accordion mt-3" id="accordianBackground">
+    <div class="card">
+      <div class="card-header bg-white" id="headingOne">
+        <h5 class="mb-0">
+          <button class="btn btn-link font-weight-bold text-dark" type="button" data-toggle="collapse" data-target="#background" aria-expanded="true" aria-controls="background">
+            Background
+          </button>
+        </h5>
+      </div>
+      <div id="background" class="collapse show" aria-labelledby="headingOne">
+        <div class="card-body">
+          <div class="row">
+            <div class="col-12 col-md-6">
+              <div class="card">
+                <div class="card-header bg-white font-weight-bold">
+                  Reporter
+                </div>
+                <ul class="list-group list-group-flush">
+                  <li class="list-group-item">Joined <span class="font-weight-bold">{{$report->reporter->created_at->diffForHumans()}}</span></li>
+                  <li class="list-group-item">Total Reports: <span class="font-weight-bold">{{App\Report::whereProfileId($report->reporter->id)->count()}}</span></li>
+                  <li class="list-group-item">Total Reported: <span class="font-weight-bold">{{App\Report::whereReportedProfileId($report->reporter->id)->count()}}</span></li>
+                </ul>
+              </div>
+            </div>
+            <div class="col-12 col-md-6">
+              <div class="card">
+                <div class="card-header bg-white font-weight-bold">
+                  Reported
+                </div>
+                <ul class="list-group list-group-flush">
+                  <li class="list-group-item">Joined <span class="font-weight-bold">{{$report->reportedUser->created_at->diffForHumans()}}</span></li>
+                  <li class="list-group-item">Total Reports: <span class="font-weight-bold">{{App\Report::whereProfileId($report->reportedUser->id)->count()}}</span></li>
+                  <li class="list-group-item">Total Reported: <span class="font-weight-bold">{{App\Report::whereReportedProfileId($report->reportedUser->id)->count()}}</span></li>
+                </ul>
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+
+{{--   <div class="accordion mt-3" id="accordianLog">
+    <div class="card">
+      <div class="card-header bg-white" id="headingTwo">
+        <h5 class="mb-0">
+          <button class="btn btn-link font-weight-bold text-dark" type="button" data-toggle="collapse" data-target="#log" aria-expanded="true" aria-controls="log">
+            Activity Log
+          </button>
+        </h5>
+      </div>
+      <div id="log" class="collapse show" aria-labelledby="headingTwo">
+        <div class="card-body" style="max-height: 200px;overflow-y: scroll;">
+            <div class="my-3 border-left-primary">
+              <p class="pl-2"><a href="#">admin</a> ignored this report. <span class="float-right pl-2 small font-weight-bold">2m</span></p>
+            </div>
+            <div class="my-3 border-left-primary">
+              <p class="pl-2"><a href="#">admin</a> ignored this report. <span class="float-right pl-2 small font-weight-bold">2m</span></p>
+            </div>
+            <div class="my-3 border-left-primary">
+              <p class="pl-2"><a href="#">admin</a> ignored this report. <span class="float-right pl-2 small font-weight-bold">2m</span></p>
+            </div>
+        </div>
+      </div>
+    </div>
+  </div> --}}
+
+
+{{--   <div class="accordion mt-3" id="accordianComments">
+    <div class="card">
+      <div class="card-header bg-white" id="headingThree">
+        <h5 class="mb-0">
+          <button class="btn btn-link font-weight-bold text-dark" type="button" data-toggle="collapse" data-target="#comments" aria-expanded="true" aria-controls="comments">
+            Comments
+          </button>
+        </h5>
+      </div>
+      <div id="comments" class="collapse show" aria-labelledby="headingThree">
+        <div class="card-body"  style="max-height: 400px; overflow-y: scroll;">
+          <div class="report-comment-wrapper">
+            <div class="my-3 report-comment">
+              <div class="card bg-primary text-white">
+                <div class="card-body">
+                  <a href="#" class="text-white font-weight-bold">[username]</a>: {{str_limit('Anim pariatur cliche reprehenderit, enim eiusmod high life accusamus terry richardson ad squid. 3 wolf moon officia aute, non cupidatat skateboard dolor brunch. Food truck quinoa nesciunt laborum eiusmod.', 150)}} <span class="float-right small p-2">2m</span>
+                </div>
+              </div>
+            </div>
+            <div class="my-3 report-comment">
+              <div class="card bg-light">
+                <div class="card-body">
+                  <a href="#" class="font-weight-bold">me</a>: {{str_limit('Anim pariatur cliche reprehenderit, enim eiusmod high life accusamus terry richardson ad squid. 3 wolf moon officia aute, non cupidatat skateboard dolor brunch. Food truck quinoa nesciunt laborum eiusmod.', 150)}} <span class="float-right small p-2">2m</span>
+                </div>
+              </div>
+            </div>
+            <div class="my-3 report-comment">
+              <div class="card bg-light">
+                <div class="card-body">
+                  <a href="#" class="font-weight-bold">me</a>: {{str_limit('Anim pariatur cliche reprehenderit, enim eiusmod high life accusamus terry richardson ad squid. 3 wolf moon officia aute, non cupidatat skateboard dolor brunch. Food truck quinoa nesciunt laborum eiusmod.', 150)}} <span class="float-right small p-2">2m</span>
+                </div>
+              </div>
+            </div>
+          </div>
+        </div>
+        <div class="card-footer">
+          <form>
+             @csrf
+             <input type="hidden" name="report_id" value="{{$report->id}}">
+             <input type="text" class="form-control" name="comment" placeholder="Add a comment here" autocomplete="off">
+          </form>
+        </div>
+      </div>
+    </div>
+  </div> --}}
+@endsection
+  
+@push('scripts')
+<script type="text/javascript">
+  
+  $(document).on('click', '.report-action-btn', function(e) {
+    e.preventDefault();
+    let el = $(this);
+    let action = el.data('action');
+    console.log(action);
+
+    axios.post(window.location.href, {
+      'action': action
+    })
+    .then(function(res) {
+      swal('Success', 'Issue updated successfully!', 'success');
+      window.location.href = window.location.href;
+    }).catch(function(res) {
+      swal('Error', res.data.msg, 'error');
+    });
+  })
+
+</script>
+@endpush

+ 74 - 0
resources/views/admin/settings/backups.blade.php

@@ -0,0 +1,74 @@
+@extends('admin.partial.template')
+
+@section('section')
+  <div class="title">
+    <h3 class="font-weight-bold">Site Backups</h3>
+  </div>
+  <hr>
+  <div class="row">
+    <div class="col-md-7">
+      <div class="card">
+        <div class="card-header bg-white font-weight-bold">Settings</div>
+        <div class="card-body">
+          <form>
+            <div class="form-group pt-3">
+              <label class="font-weight-bold text-muted small">Auto Backup Enabled</label>
+              <div class="switch switch-sm">
+                <input type="checkbox" class="switch" id="cw-switch" name="cw">
+                <label for="cw-switch" class="small font-weight-bold">(Default off)</label>
+              </div>
+              <small class="form-text text-muted">
+                Enable automated backups with your own strategy.
+              </small>
+            </div>
+            <div class="form-group pt-3">
+              <label class="font-weight-bold text-muted small">Frequency</label>
+              <select class="form-control">
+                <option>Hourly (1h)</option>
+                <option selected="">Nightly (24h)</option>
+                <option>Weekly (7d)</option>
+                <option>Monthly (1m)</option>
+              </select>
+              <small class="form-text text-muted">
+                Select the backup frequency.
+              </small>
+            </div>
+            <div class="form-group pt-3">
+              <label class="font-weight-bold text-muted small">Storage Filesystem</label>
+              <select class="form-control">
+                <option>Local</option>
+                <option disabled="">S3 (Not configured)</option>
+              </select>
+              <small class="form-text text-muted">
+                You can use local, S3, or any S3 compatible object storage API to store backups.
+              </small>
+            </div>
+          </form>
+        </div>
+      </div>
+    </div>
+    <div class="col-md-5">
+      <div class="card">
+        <div class="card-header bg-white font-weight-bold">Current Backups</div>
+        <div class="list-group list-group-flush">
+          @foreach($files as $file)
+          @if($file->isFile())
+          <li class="list-group-item pb-0">
+            <p class="font-weight-bold mb-0 text-truncate">{{$file->getFilename()}}</p>
+            <p class="mb-0 small text-muted font-weight-bold">
+              <span>
+                Size: {{App\Util\Lexer\PrettyNumber::convert($file->getSize())}}
+              </span>
+              <span class="float-right">
+                Created: {{\Carbon\Carbon::createFromTimestamp($file->getMTime())->diffForHumans()}}</p>
+              </span>
+            </p>
+          </li>
+          @endif
+          @endforeach
+        </div>
+      </div>
+      
+    </div>
+  </div>
+@endsection

+ 9 - 0
resources/views/admin/settings/maintenance.blade.php

@@ -0,0 +1,9 @@
+@extends('admin.partial.template')
+
+@section('section')
+  <div class="title">
+    <h3 class="font-weight-bold">Maintenance</h3>
+  </div>
+  <hr>
+
+@endsection

+ 30 - 0
resources/views/admin/settings/storage.blade.php

@@ -0,0 +1,30 @@
+@extends('admin.partial.template')
+
+@section('section')
+  <div class="title">
+    <h3 class="font-weight-bold">Storage</h3>
+  </div>
+  <hr>
+
+  <div class="card">
+  	<div class="card-body">
+		<div class="progress">
+		  <div class="progress-bar" role="progressbar" style="width: {{$storage->percentUsed}}%" aria-valuenow="{{$storage->percentUsed}}" aria-valuemin="0" aria-valuemax="100"></div>
+		</div>
+		<div class="d-flex justify-content-between">
+			<span class="font-weight-bold">
+			 Used: {{$storage->prettyTotal}}
+			</span>
+			<span class="font-weight-bold">
+			{{$storage->percentUsed}}% Used	
+			</span>
+			<span class="font-weight-bold">
+			  Free: {{$storage->prettyFree}}
+			</span>
+		</div>
+  	</div>
+  	<div class="card-footer bg-white font-weight-bold text-center">
+  		Total Disk Space
+  	</div>
+  </div>
+@endsection

+ 39 - 0
resources/views/admin/settings/system.blade.php

@@ -0,0 +1,39 @@
+@extends('admin.partial.template')
+
+@section('section')
+  <div class="title">
+    <h3 class="font-weight-bold">System</h3>
+  </div>
+  <hr>
+  <div class="row">
+  	<div class="col-12 col-md-6">
+  		<div class="card mb-3">
+  			<div class="card-body text-center">
+  				<p class="font-weight-ultralight display-4 mb-0">{{config('pixelfed.version')}}</p>
+  			</div>
+  			<div class="card-footer font-weight-bold text-center bg-white">Pixelfed</div>
+  		</div>
+	
+  		<div class="card mb-3">
+  			<div class="card-body text-center">
+  				<p class="font-weight-ultralight display-4 mb-0">{{DB::select( DB::raw("select version()") )[0]->{'version()'} }}</p>
+  			</div>
+  			<div class="card-footer font-weight-bold text-center bg-white">MySQL</div>
+  		</div>
+  	</div>
+  	<div class="col-12 col-md-6">
+  		<div class="card mb-3">
+  			<div class="card-body text-center">
+  				<p class="font-weight-ultralight display-4 mb-0">{{phpversion()}}</p>
+  			</div>
+  			<div class="card-footer font-weight-bold text-center bg-white">PHP</div>
+  		</div>  	
+{{--   		<div class="card mb-3">
+  			<div class="card-body text-center">
+  				<p class="font-weight-ultralight display-4 mb-0"></p>
+  			</div>
+  			<div class="card-footer font-weight-bold text-center bg-white">Redis</div>
+  		</div> --}}
+  	</div>
+  </div>
+@endsection

+ 40 - 0
resources/views/layouts/anon.blade.php

@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<html lang="{{ app()->getLocale() }}">
+<head>
+    
+    <meta charset="utf-8">
+    <meta http-equiv="X-UA-Compatible" content="IE=edge">
+    <meta name="viewport" content="width=device-width, initial-scale=1">
+    <meta name="csrf-token" content="{{ csrf_token() }}">
+
+    <meta name="robots" content="noimageindex, noarchive">
+    <meta name="mobile-web-app-capable" content="yes">
+
+    <title>{{ $title or config('app.name', 'Laravel') }}</title>
+
+    @if(isset($title))<meta property="og:site_name" content="{{ config('app.name', 'Laravel') }}">
+    <meta property="og:title" content="{{ $title or config('app.name', 'Laravel') }}">
+    <meta property="og:type" content="article">
+    <meta property="og:url" content="{{request()->url()}}">
+    @endif
+
+    @stack('meta')
+
+    <meta name="medium" content="image">
+    <meta name="theme-color" content="#10c5f8">
+    <meta name="apple-mobile-web-app-capable" content="yes">
+
+    <link rel="canonical" href="{{request()->url()}}">
+    <link href="{{ mix('css/app.css') }}" rel="stylesheet">
+    @stack('styles')
+</head>
+<body class="">
+    @include('layouts.partial.noauthnav')
+    <main id="content">
+        @yield('content')
+    </main>
+    @include('layouts.partial.footer')
+    <script type="text/javascript" src="{{ mix('js/app.js') }}"></script>
+    @stack('scripts')
+</body>
+</html>

+ 8 - 0
resources/views/layouts/partial/noauthnav.blade.php

@@ -0,0 +1,8 @@
+<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('/') }}" title="{{ config('app.name', 'Laravel') }} Logo">
+            <img src="/img/pixelfed-icon-color.svg" height="30px" class="px-2">
+            <span class="font-weight-bold mb-0" style="font-size:20px;">{{ config('app.name', 'Laravel') }}</span>
+        </a>
+    </div>
+</nav>

+ 32 - 0
resources/views/profile/partial/private-info.blade.php

@@ -0,0 +1,32 @@
+<div class="bg-white py-5 border-bottom">
+  <div class="container">
+    <div class="row">
+      <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-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-ultralight h1">{{$user->username}}</span>
+          </div>
+          <div class="profile-stats pb-3 d-inline-flex lead">
+            <div class="font-weight-light pr-5">
+              <span class="font-weight-bold">{{$user->statuses()->whereNull('reblog_of_id')->whereNull('in_reply_to_id')->count()}}</span> 
+              Posts
+            </div>
+          </div>
+          <p class="lead mb-0">
+            <span class="font-weight-bold">{{$user->name}}</span> 
+            @if($user->remote_url)
+            <span class="badge badge-info">REMOTE PROFILE</span>
+            @endif
+          </p>
+          <p class="mb-0 lead">{{$user->bio}}</p>
+          <p class="mb-0"><a href="{{$user->website}}" class="font-weight-bold" rel="external nofollow noopener" target="_blank">{{str_limit($user->website, 30)}}</a></p>
+        </div>
+      </div>
+    </div>
+  </div>
+</div>

+ 4 - 0
resources/views/profile/partial/user-info.blade.php

@@ -57,18 +57,22 @@
               Posts
               </a>
             </div>
+            @if($settings->show_profile_follower_count)
             <div class="font-weight-light pr-5">
               <a class="text-dark" href="{{$user->url('/followers')}}">
               <span class="font-weight-bold">{{$user->followerCount(true)}}</span> 
               Followers
               </a>
             </div>
+            @endif
+            @if($settings->show_profile_following_count)
             <div class="font-weight-light pr-5">
               <a class="text-dark" href="{{$user->url('/following')}}">
               <span class="font-weight-bold">{{$user->followingCount(true)}}</span> 
               Following
               </a>
             </div>
+            @endif
           </div>
           <p class="lead mb-0">
             <span class="font-weight-bold">{{$user->name}}</span> 

+ 33 - 0
resources/views/profile/private.blade.php

@@ -0,0 +1,33 @@
+@extends('layouts.app',['title' => $user->username . " on " . config('app.name')])
+
+@section('content')
+
+@include('profile.partial.private-info')
+
+<div class="container">
+  <div class="profile-timeline mt-2 mt-md-4">
+    <div class="card">
+      <div class="card-body py-5">
+        <p class="text-center lead font-weight-bold">
+          {{__('profile.privateProfileWarning')}}
+        </p>
+
+        @if(Auth::check())
+        <p class="text-center mb-0">{{ __('profile.alreadyFollow', ['username'=>$user->username])}}</p>
+        <p class="text-center mb-0"><a href="{{route('login')}}">{{__('Log in')}}</a></p>
+        <p class="text-center mb-0">{{__('profile.loginToSeeProfile')}}</p>
+        @endif
+      </div>
+    </div>
+  </div>
+</div>
+
+@endsection
+
+@push('meta')
+<meta property="og:description" content="{{$user->bio}}">
+<meta property="og:image" content="{{$user->avatarUrl()}}">
+@if($user->remote_url)
+<meta name="robots" content="noindex, nofollow">
+@endif
+@endpush

+ 1 - 0
resources/views/profile/show.blade.php

@@ -74,6 +74,7 @@
 
 @push('meta')<meta property="og:description" content="{{$user->bio}}">
     <meta property="og:image" content="{{$user->avatarUrl()}}">
+    <link href="{{$user->permalink('.atom')}}" rel="alternate" title="{{$user->username}} on PixelFed" type="application/atom+xml">
   @if(false == $settings->crawlable || $user->remote_url)
   <meta name="robots" content="noindex, nofollow">
   @endif

+ 45 - 0
resources/views/report/abusive/comment.blade.php

@@ -0,0 +1,45 @@
+@extends('layouts.app')
+
+@section('content')
+
+<div class="container mt-4 mb-5 pb-5">
+  <div class="col-12 col-md-8 offset-md-2">
+    <div class="card">
+      <div class="card-header lead font-weight-bold">
+        Report Abusive/Harmful Comment
+      </div>
+      <div class="card-body">
+        <div class="row">
+          <div class="col-12 col-md-10 offset-md-1 my-3">
+            <form method="post" action="{{route('report.form')}}">
+              @csrf
+              <input type="hidden" name="report" value="abusive"></input>
+              <input type="hidden" name="type" value="{{request()->query('type')}}"></input>
+              <input type="hidden" name="id" value="{{request()->query('id')}}"></input>
+              <div class="form-group row">
+                <label class="col-sm-3 col-form-label font-weight-bold text-right">Message</label>
+                <div class="col-sm-9">
+                  <textarea class="form-control" name="msg" placeholder="Add an optional message for mods/admins" rows="4"></textarea>
+                </div>
+              </div>
+              <hr>
+              <div class="form-group row">
+                <div class="col-12">
+                  <button type="submit" class="btn btn-primary btn-block font-weight-bold">Submit</button>
+                </div>
+              </div>
+            </form>
+          </div>
+
+          <div class="col-12 col-md-8 offset-md-2">
+            <p><a class="font-weight-bold" href="#">
+              Learn more
+            </a> about our reporting guidelines and policy.</p>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</div>
+
+@endsection

+ 45 - 0
resources/views/report/abusive/post.blade.php

@@ -0,0 +1,45 @@
+@extends('layouts.app')
+
+@section('content')
+
+<div class="container mt-4 mb-5 pb-5">
+  <div class="col-12 col-md-8 offset-md-2">
+    <div class="card">
+      <div class="card-header lead font-weight-bold">
+        Report Abusive/Harmful Post
+      </div>
+      <div class="card-body">
+        <div class="row">
+          <div class="col-12 col-md-10 offset-md-1 my-3">
+            <form method="post" action="{{route('report.form')}}">
+              @csrf
+              <input type="hidden" name="report" value="abusive"></input>
+              <input type="hidden" name="type" value="{{request()->query('type')}}"></input>
+              <input type="hidden" name="id" value="{{request()->query('id')}}"></input>
+              <div class="form-group row">
+                <label class="col-sm-3 col-form-label font-weight-bold text-right">Message</label>
+                <div class="col-sm-9">
+                  <textarea class="form-control" name="msg" placeholder="Add an optional message for mods/admins" rows="4"></textarea>
+                </div>
+              </div>
+              <hr>
+              <div class="form-group row">
+                <div class="col-12">
+                  <button type="submit" class="btn btn-primary btn-block font-weight-bold">Submit</button>
+                </div>
+              </div>
+            </form>
+          </div>
+
+          <div class="col-12 col-md-8 offset-md-2">
+            <p><a class="font-weight-bold" href="#">
+              Learn more
+            </a> about our reporting guidelines and policy.</p>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</div>
+
+@endsection

+ 45 - 0
resources/views/report/abusive/profile.blade.php

@@ -0,0 +1,45 @@
+@extends('layouts.app')
+
+@section('content')
+
+<div class="container mt-4 mb-5 pb-5">
+  <div class="col-12 col-md-8 offset-md-2">
+    <div class="card">
+      <div class="card-header lead font-weight-bold">
+        Report Abusive/Harmful Profile
+      </div>
+      <div class="card-body">
+        <div class="row">
+          <div class="col-12 col-md-10 offset-md-1 my-3">
+            <form method="post" action="{{route('report.form')}}">
+              @csrf
+              <input type="hidden" name="report" value="abusive"></input>
+              <input type="hidden" name="type" value="{{request()->query('type')}}"></input>
+              <input type="hidden" name="id" value="{{request()->query('id')}}"></input>
+              <div class="form-group row">
+                <label class="col-sm-3 col-form-label font-weight-bold text-right">Message</label>
+                <div class="col-sm-9">
+                  <textarea class="form-control" name="msg" placeholder="Add an optional message for mods/admins" rows="4"></textarea>
+                </div>
+              </div>
+              <hr>
+              <div class="form-group row">
+                <div class="col-12">
+                  <button type="submit" class="btn btn-primary btn-block font-weight-bold">Submit</button>
+                </div>
+              </div>
+            </form>
+          </div>
+
+          <div class="col-12 col-md-8 offset-md-2">
+            <p><a class="font-weight-bold" href="#">
+              Learn more
+            </a> about our reporting guidelines and policy.</p>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</div>
+
+@endsection

+ 70 - 21
resources/views/report/form.blade.php

@@ -5,41 +5,90 @@
 <div class="container mt-4 mb-5 pb-5">
   <div class="col-12 col-md-8 offset-md-2">
 
-    <div class="card my-5">
-      <div class="card-body">
-        <p class="mb-0 font-weight-bold">This feature is not yet ready for production. Please try again later.</p>
-      </div>
-    </div>
-
-    <div class="card sr-only">
-      <div class="card-header lead font-weight-bold">
+    <div class="card">
+      <div class="card-header lead font-weight-bold bg-white">
         Report
       </div>
       <div class="card-body">
-        <div class="p-5 text-center">
-          <p class="lead">Please select one of the following options.</p>
+        <div class="p-3 text-center">
+          <p class="lead">Please select one of the following options. </p>
         </div>
         <div class="row">
           <div class="col-12 col-md-8 offset-md-2 my-3">
-            <p><a class="btn btn-light btn-block p-4 font-weight-bold" disabled>
-              Im not interested in this content
+            <p><a class="btn btn-light btn-block p-4 font-weight-bold" href="{{route('report.not-interested', ['type' => request()->query('type'),'id' => request()->query('id')])}}">
+              I'm not interested in this content
             </a></p>
           </div>
-          <div class="col-12 col-md-8 offset-md-2 my-3">
-            <p><a class="btn btn-light btn-block p-4 font-weight-bold">
-              It’s spam
+          @switch(request()->query('type'))
+
+          @case('comment')
+          <div class="col-12 col-md-8 offset-md-2 mb-3">
+            <p><a class="btn btn-light btn-block p-4 font-weight-bold" href="{{route('report.spam.comment', ['type' => request()->query('type'),'id' => request()->query('id')])}}">
+              This comment contains spam
             </a></p>
           </div>
           <div class="col-12 col-md-8 offset-md-2 my-3">
-            <p><a class="btn btn-light btn-block p-4 font-weight-bold">
-              It displays a sensitive image
-            </a></p>
+            <p>
+              <a class="btn btn-light btn-block p-4 font-weight-bold" href="{{route('report.sensitive.comment', ['type' => request()->query('type'),'id' => request()->query('id')])}}">
+                This comment contains sensitive content
+              </a>
+            </p>
           </div>
           <div class="col-12 col-md-8 offset-md-2 my-3">
-            <p><a class="btn btn-light btn-block p-4 font-weight-bold">
-              It’s abusive or harmful
-            </a></p>
+            <p>
+              <a class="btn btn-light btn-block p-4 font-weight-bold" href="{{route('report.abusive.comment', ['type' => request()->query('type'),'id' => request()->query('id')])}}">
+                It’s abusive or harmful
+              </a>
+            </p>
+          </div>
+          @break
+          @case('post')
+          <div class="col-12 col-md-8 offset-md-2 mb-3">
+            <p>
+              <a class="btn btn-light btn-block p-4 font-weight-bold" href="{{route('report.spam.post', ['type' => request()->query('type'),'id' => request()->query('id')])}}">
+                This post contains spam
+              </a>
+            </p>
+          </div>
+          <div class="col-12 col-md-8 offset-md-2 my-3">
+            <p>
+              <a class="btn btn-light btn-block p-4 font-weight-bold" href="{{route('report.sensitive.post', ['type' => request()->query('type'),'id' => request()->query('id')])}}">
+                This post contains sensitive content
+              </a>
+            </p>
+          </div>
+          <div class="col-12 col-md-8 offset-md-2 my-3">
+            <p>
+              <a class="btn btn-light btn-block p-4 font-weight-bold" href="{{route('report.abusive.post', ['type' => request()->query('type'),'id' => request()->query('id')])}}">
+                This post is abusive or harmful
+              </a>
+            </p>
+          </div>
+          @break
+          @case('user')
+          <div class="col-12 col-md-8 offset-md-2 mb-3">
+            <p>
+              <a class="btn btn-light btn-block p-4 font-weight-bold" href="{{route('report.spam.profile', ['type' => request()->query('type'),'id' => request()->query('id')])}}">
+                This users profile contains spam
+              </a>
+            </p>
+          </div>
+          <div class="col-12 col-md-8 offset-md-2 my-3">
+            <p>
+              <a class="btn btn-light btn-block p-4 font-weight-bold" href="{{route('report.sensitive.profile', ['type' => request()->query('type'),'id' => request()->query('id')])}}">
+                This users profile contains sensitive content
+              </a>
+            </p>
+          </div>
+          <div class="col-12 col-md-8 offset-md-2 my-3">
+            <p>
+              <a class="btn btn-light btn-block p-4 font-weight-bold" href="{{route('report.abusive.profile', ['type' => request()->query('type'),'id' => request()->query('id')])}}">
+                This profile is abusive or harmful
+              </a>
+            </p>
           </div>
+          @break
+          @endswitch
           <div class="col-12 col-md-8 offset-md-2 my-3">
             <p><a class="font-weight-bold" href="#">
               Learn more

+ 5 - 0
resources/views/report/not-interested.blade.php

@@ -12,6 +12,11 @@
         <div class="p-5 text-center">
           <p class="lead">You can <b class="font-weight-bold">unfollow</b> or <b class="font-weight-bold">mute</b> a user or hashtag from appearing in your timeline. Unless the content violates our terms of service, there is nothing we can do to remove it.</p>
         </div>
+        <div class="col-12 col-md-8 offset-md-2">
+          <p><a class="font-weight-bold" href="#">
+            Learn more
+          </a> about our reporting guidelines and policy.</p>
+        </div>
       </div>
     </div>
   </div>

+ 45 - 0
resources/views/report/sensitive/comment.blade.php

@@ -0,0 +1,45 @@
+@extends('layouts.app')
+
+@section('content')
+
+<div class="container mt-4 mb-5 pb-5">
+  <div class="col-12 col-md-8 offset-md-2">
+    <div class="card">
+      <div class="card-header lead font-weight-bold">
+        Report Sensitive Comment
+      </div>
+      <div class="card-body">
+        <div class="row">
+          <div class="col-12 col-md-10 offset-md-1 my-3">
+            <form method="post" action="{{route('report.form')}}">
+              @csrf
+              <input type="hidden" name="report" value="sensitive"></input>
+              <input type="hidden" name="type" value="{{request()->query('type')}}"></input>
+              <input type="hidden" name="id" value="{{request()->query('id')}}"></input>
+              <div class="form-group row">
+                <label class="col-sm-3 col-form-label font-weight-bold text-right">Message</label>
+                <div class="col-sm-9">
+                  <textarea class="form-control" name="msg" placeholder="Add an optional message for mods/admins" rows="4"></textarea>
+                </div>
+              </div>
+              <hr>
+              <div class="form-group row">
+                <div class="col-12">
+                  <button type="submit" class="btn btn-primary btn-block font-weight-bold">Submit</button>
+                </div>
+              </div>
+            </form>
+          </div>
+
+          <div class="col-12 col-md-8 offset-md-2">
+            <p><a class="font-weight-bold" href="#">
+              Learn more
+            </a> about our reporting guidelines and policy.</p>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</div>
+
+@endsection

+ 45 - 0
resources/views/report/sensitive/post.blade.php

@@ -0,0 +1,45 @@
+@extends('layouts.app')
+
+@section('content')
+
+<div class="container mt-4 mb-5 pb-5">
+  <div class="col-12 col-md-8 offset-md-2">
+    <div class="card">
+      <div class="card-header lead font-weight-bold">
+        Report Sensitive Post
+      </div>
+      <div class="card-body">
+        <div class="row">
+          <div class="col-12 col-md-10 offset-md-1 my-3">
+            <form method="post" action="{{route('report.form')}}">
+              @csrf
+              <input type="hidden" name="report" value="sensitive"></input>
+              <input type="hidden" name="type" value="{{request()->query('type')}}"></input>
+              <input type="hidden" name="id" value="{{request()->query('id')}}"></input>
+              <div class="form-group row">
+                <label class="col-sm-3 col-form-label font-weight-bold text-right">Message</label>
+                <div class="col-sm-9">
+                  <textarea class="form-control" name="msg" placeholder="Add an optional message for mods/admins" rows="4"></textarea>
+                </div>
+              </div>
+              <hr>
+              <div class="form-group row">
+                <div class="col-12">
+                  <button type="submit" class="btn btn-primary btn-block font-weight-bold">Submit</button>
+                </div>
+              </div>
+            </form>
+          </div>
+
+          <div class="col-12 col-md-8 offset-md-2">
+            <p><a class="font-weight-bold" href="#">
+              Learn more
+            </a> about our reporting guidelines and policy.</p>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</div>
+
+@endsection

+ 45 - 0
resources/views/report/sensitive/profile.blade.php

@@ -0,0 +1,45 @@
+@extends('layouts.app')
+
+@section('content')
+
+<div class="container mt-4 mb-5 pb-5">
+  <div class="col-12 col-md-8 offset-md-2">
+    <div class="card">
+      <div class="card-header lead font-weight-bold">
+        Report Sensitive Profile
+      </div>
+      <div class="card-body">
+        <div class="row">
+          <div class="col-12 col-md-10 offset-md-1 my-3">
+            <form method="post" action="{{route('report.form')}}">
+              @csrf
+              <input type="hidden" name="report" value="sensitive"></input>
+              <input type="hidden" name="type" value="{{request()->query('type')}}"></input>
+              <input type="hidden" name="id" value="{{request()->query('id')}}"></input>
+              <div class="form-group row">
+                <label class="col-sm-3 col-form-label font-weight-bold text-right">Message</label>
+                <div class="col-sm-9">
+                  <textarea class="form-control" name="msg" placeholder="Add an optional message for mods/admins" rows="4"></textarea>
+                </div>
+              </div>
+              <hr>
+              <div class="form-group row">
+                <div class="col-12">
+                  <button type="submit" class="btn btn-primary btn-block font-weight-bold">Submit</button>
+                </div>
+              </div>
+            </form>
+          </div>
+
+          <div class="col-12 col-md-8 offset-md-2">
+            <p><a class="font-weight-bold" href="#">
+              Learn more
+            </a> about our reporting guidelines and policy.</p>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</div>
+
+@endsection

+ 12 - 3
resources/views/report/spam.blade.php

@@ -13,21 +13,30 @@
           <p class="lead">Please select one of the following options.</p>
         </div>
         <div class="row">
-          <div class="col-12 col-md-8 offset-md-2 my-3">
+          @switch(request()->query('type'))
+
+          @case('comment')
+          <div class="col-12 col-md-8 offset-md-2 mb-3">
             <p><a class="btn btn-light btn-block p-4 font-weight-bold" href="{{route('report.spam.comment')}}">
               This comment contains spam
             </a></p>
           </div>
-          <div class="col-12 col-md-8 offset-md-2 my-3">
+          @break
+          @case('post')
+          <div class="col-12 col-md-8 offset-md-2 mb-3">
             <p><a class="btn btn-light btn-block p-4 font-weight-bold" href="{{route('report.spam.post')}}">
               This post contains spam
             </a></p>
           </div>
-          <div class="col-12 col-md-8 offset-md-2 my-3">
+          @break
+          @case('user')
+          <div class="col-12 col-md-8 offset-md-2 mb-3">
             <p><a class="btn btn-light btn-block p-4 font-weight-bold" href="{{route('report.spam.profile')}}">
               This users profile contains spam
             </a></p>
           </div>
+          @break
+          @endswitch
           <div class="col-12 col-md-8 offset-md-2 my-3">
             <p><a class="font-weight-bold" href="#">
               Learn more

+ 21 - 18
resources/views/report/spam/post.blade.php

@@ -9,26 +9,29 @@
         Report Post Spam
       </div>
       <div class="card-body">
-        <div class="p-5 text-center">
-          <p class="lead">Please select one of the following options.</p>
-        </div>
         <div class="row">
-          <div class="col-12 col-md-8 offset-md-2 my-3">
-            <p><a class="btn btn-light btn-block p-4 font-weight-bold" href="#">
-              This comment contains spam
-            </a></p>
-          </div>
-          <div class="col-12 col-md-8 offset-md-2 my-3">
-            <p><a class="btn btn-light btn-block p-4 font-weight-bold" href="#">
-              This post contains spam
-            </a></p>
+          <div class="col-12 col-md-10 offset-md-1 my-3">
+            <form method="post" action="{{route('report.form')}}">
+              @csrf
+              <input type="hidden" name="report" value="spam"></input>
+              <input type="hidden" name="type" value="{{request()->query('type')}}"></input>
+              <input type="hidden" name="id" value="{{request()->query('id')}}"></input>
+              <div class="form-group row">
+                <label class="col-sm-3 col-form-label font-weight-bold text-right">Message</label>
+                <div class="col-sm-9">
+                  <textarea class="form-control" name="msg" placeholder="Add an optional message for mods/admins" rows="4"></textarea>
+                </div>
+              </div>
+              <hr>
+              <div class="form-group row">
+                <div class="col-12">
+                  <button type="submit" class="btn btn-primary btn-block font-weight-bold">Submit</button>
+                </div>
+              </div>
+            </form>
           </div>
-          <div class="col-12 col-md-8 offset-md-2 my-3">
-            <p><a class="btn btn-light btn-block p-4 font-weight-bold" href="#">
-              This users profile contains spam
-            </a></p>
-          </div>
-          <div class="col-12 col-md-8 offset-md-2 my-3">
+
+          <div class="col-12 col-md-8 offset-md-2">
             <p><a class="font-weight-bold" href="#">
               Learn more
             </a> about our reporting guidelines and policy.</p>

+ 1 - 1
resources/views/report/spam/profile.blade.php

@@ -28,7 +28,7 @@
               This users profile contains spam
             </a></p>
           </div>
-          <div class="col-12 col-md-8 offset-md-2 my-3">
+          <div class="col-12 col-md-8 offset-md-2">
             <p><a class="font-weight-bold" href="#">
               Learn more
             </a> about our reporting guidelines and policy.</p>

+ 4 - 1
resources/views/settings/home.blade.php

@@ -39,6 +39,9 @@
       <label for="bio" class="col-sm-3 col-form-label font-weight-bold text-right">Bio</label>
       <div class="col-sm-9">
         <textarea class="form-control" id="bio" name="bio" placeholder="Add a bio here" rows="2">{{Auth::user()->profile->bio}}</textarea>
+        <small class="form-text text-muted">
+          Max length: {{config('pixelfed.max_bio_length')}} characters.
+        </small>
       </div>
     </div>
     <div class="pt-5">
@@ -134,4 +137,4 @@
     });
   });
 </script>
-@endpush
+@endpush

+ 91 - 0
resources/views/status/edit.blade.php

@@ -0,0 +1,91 @@
+@extends('layouts.app')
+
+@section('content')
+
+<div class="container">
+  <div class="col-12 col-md-8 offset-md-2 pt-4">
+
+    <div class="card">
+      <div class="card-header bg-white font-weight-bold d-flex justify-content-between align-items-center">
+        <span>Edit Status</span>
+        <a class="btn btn-outline-primary btn-sm font-weight-bold" href="{{$status->url()}}">Back to post</a>
+      </div>
+      <div class="card-body">
+          @csrf
+          <div class="form-group mb-0">
+            <label class="font-weight-bold text-muted small">CW/NSFW</label>
+            <div class="switch switch-sm">
+              <input type="checkbox" class="switch" id="cw-switch" name="cw" {{$status->is_nsfw==true?'checked=""':''}} disabled="">
+              <label for="cw-switch" class="small font-weight-bold">(Default off)</label>
+            </div>
+          </div>
+      </div>
+    </div>
+
+    @foreach($status->media()->orderBy('order')->get() as $media)
+    <div class="card mt-4 media-card">
+      <div class="card-header bg-white font-weight-bold">
+        Media #{{$media->order}}
+      </div>
+      <div class="card-body p-0">
+      <form method="post" enctype="multipart/form-data" class="media-form">
+        @csrf
+        <input type="hidden" name="media_id" value="{{$media->id}}">
+        <div class="filter-wrapper {{$media->filter_class}}" data-filter="{{$media->filter_class}}">
+          <img class="img-fluid" src="{{$media->url()}}" width="100%">
+        </div>
+        <div class="p-3">
+          <div class="form-group">
+            <label class="font-weight-bold text-muted small">Description</label>
+            <input class="form-control" name="media_caption" value="{{$media->caption}}" placeholder="Add a descriptive caption for screenreaders" autocomplete="off">
+          </div>
+          <div class="form-group form-filters" data-filter="{{$media->filter_class}}">
+            <label for="filterSelectDropdown" class="font-weight-bold text-muted small">Select Filter</label>
+            <select class="form-control filter-dropdown" name="media_filter"><option value="" selected="">No Filter</option><option value="filter-1977">1977</option><option value="filter-aden">Aden</option><option value="filter-amaro">Amaro</option><option value="filter-ashby">Ashby</option><option value="filter-brannan">Brannan</option><option value="filter-brooklyn">Brooklyn</option><option value="filter-charmes">Charmes</option><option value="filter-clarendon">Clarendon</option><option value="filter-crema">Crema</option><option value="filter-dogpatch">Dogpatch</option><option value="filter-earlybird">Earlybird</option><option value="filter-gingham">Gingham</option><option value="filter-ginza">Ginza</option><option value="filter-hefe">Hefe</option><option value="filter-helena">Helena</option><option value="filter-hudson">Hudson</option><option value="filter-inkwell">Inkwell</option><option value="filter-kelvin">Kelvin</option><option value="filter-juno">Kuno</option><option value="filter-lark">Lark</option><option value="filter-lofi">Lo-Fi</option><option value="filter-ludwig">Ludwig</option><option value="filter-maven">Maven</option><option value="filter-mayfair">Mayfair</option><option value="filter-moon">Moon</option><option value="filter-nashville">Nashville</option><option value="filter-perpetua">Perpetua</option><option value="filter-poprocket">Poprocket</option><option value="filter-reyes">Reyes</option><option value="filter-rise">Rise</option><option value="filter-sierra">Sierra</option><option value="filter-skyline">Skyline</option><option value="filter-slumber">Slumber</option><option value="filter-stinson">Stinson</option><option value="filter-sutro">Sutro</option><option value="filter-toaster">Toaster</option><option value="filter-valencia">Valencia</option><option value="filter-vesper">Vesper</option><option value="filter-walden">Walden</option><option value="filter-willow">Willow</option><option value="filter-xpro-ii">X-Pro II</option></select>
+          </div>
+          <hr>
+          <div class="form-group d-flex justify-content-between align-items-center mb-0">
+            <p class="text-muted font-weight-bold mb-0 small">Last Updated: {{$media->updated_at->diffForHumans()}}</p>
+            <button type="submit" class="btn btn-primary btn-sm font-weight-bold px-4">Update</button>
+          </div>  
+        </div>
+      </form>
+      </div>
+    </div>
+    @endforeach
+
+  </div>
+</div>
+@endsection
+
+@push('scripts')
+<script type="text/javascript">
+  
+  $('.form-filters').each(function(i,d) {
+    let el = $(d);
+    let filter = el.data('filter');
+    if(filter) {
+      var opt = el.find('option[value='+filter+']')[0];
+      $(opt).attr('selected','');
+    }
+  });
+
+  $('.media-form').on('submit', function(e){
+    e.preventDefault();
+    let el = $(this);
+    let id = el.find('input[name=media_id]').val();
+    let caption = el.find('input[name=media_caption]').val();
+    let filter = el.find('.filter-dropdown option:selected').val();
+    axios.post(window.location.href, {
+      'id': id,
+      'caption': caption,
+      'filter': filter
+    }).then((res) => {
+      swal('Success!', 'You have successfully updated your post', 'success');
+    }).catch((err) => {
+      swal('Something went wrong', 'An error occured, please try again later', 'error');
+    });
+  });
+
+</script>
+@endpush

+ 65 - 0
resources/views/status/show/album.blade.php

@@ -0,0 +1,65 @@
+@extends('layouts.app',['title' => $user->username . " posted a photo: " . $status->likes_count . " likes, " . $status->comments_count . " comments" ])
+
+@section('content')
+
+<div class="container px-0 mt-md-4">
+  <div class="card card-md-rounded-0 status-container orientation-{{$status->firstMedia()->orientation ?? 'unknown'}}">
+    <div class="row mx-0">
+    <div class="d-flex d-md-none align-items-center justify-content-between card-header bg-white w-100">
+      <a href="{{$user->url()}}" class="d-flex align-items-center status-username text-truncate" data-toggle="tooltip" data-placement="bottom" title="{{$user->username}}">
+        <div class="status-avatar mr-2">
+          <img src="{{$user->avatarUrl()}}" width="24px" height="24px" style="border-radius:12px;">
+        </div>
+        <div class="username">
+          <span class="username-link font-weight-bold text-dark">{{$user->username}}</span>
+        </div>
+      </a>
+     </div>
+      <div class="col-12 col-md-8 status-photo px-0">
+        @if($status->is_nsfw)
+        <details class="details-animated">
+          <summary>
+            <p class="mb-0 lead font-weight-bold">CW / NSFW / Hidden Media</p>
+            <p class="font-weight-light">(click to show)</p>
+          </summary>
+          @endif
+            <div id="photoCarousel" class="carousel slide carousel-fade" data-ride="carousel">
+              <ol class="carousel-indicators">
+                @for($i = 0; $i < $status->media_count; $i++)
+                <li data-target="#photoCarousel" data-slide-to="{{$i}}" class="{{$i == 0 ? 'active' : ''}}"></li>
+                @endfor
+              </ol>
+              <div class="carousel-inner">
+                @foreach($status->media()->orderBy('order')->get() as $media)
+                <div class="carousel-item {{$loop->iteration == 1 ? 'active' : ''}}">
+                  <figure class="{{$media->filter_class}}">
+                    <img class="d-block w-100" src="{{$media->url()}}" title="{{$media->caption}}" data-toggle="tooltip" data-placement="bottom">
+                  </figure>
+                </div>
+                @endforeach
+              </div>
+              <a class="carousel-control-prev" href="#photoCarousel" role="button" data-slide="prev">
+                <span class="carousel-control-prev-icon" aria-hidden="true"></span>
+                <span class="sr-only">Previous</span>
+              </a>
+              <a class="carousel-control-next" href="#photoCarousel" role="button" data-slide="next">
+                <span class="carousel-control-next-icon" aria-hidden="true"></span>
+                <span class="sr-only">Next</span>
+              </a>
+            </div>
+        @if($status->is_nsfw)
+          </details>
+        @endif
+      </div>
+      @include('status.show.sidebar')
+    </div>
+  </div>
+</div>
+
+@endsection
+
+@push('meta')
+  <meta property="og:description" content="{{ $status->caption }}">
+  <meta property="og:image" content="{{$status->mediaUrl()}}">
+  <link href='{{$status->url()}}' rel='alternate' type='application/activity+json'>
+@endpush

+ 46 - 0
resources/views/status/show/photo.blade.php

@@ -0,0 +1,46 @@
+@extends('layouts.app',['title' => $user->username . " posted a photo: " . $status->likes_count . " likes, " . $status->comments_count . " comments" ])
+
+@section('content')
+
+<div class="container px-0 mt-md-4">
+  <div class="card card-md-rounded-0 status-container orientation-{{$status->firstMedia()->orientation ?? 'unknown'}}">
+    <div class="row mx-0">
+    <div class="d-flex d-md-none align-items-center justify-content-between card-header bg-white w-100">
+      <a href="{{$user->url()}}" class="d-flex align-items-center status-username text-truncate" data-toggle="tooltip" data-placement="bottom" title="{{$user->username}}">
+        <div class="status-avatar mr-2">
+          <img src="{{$user->avatarUrl()}}" width="24px" height="24px" style="border-radius:12px;">
+        </div>
+        <div class="username">
+          <span class="username-link font-weight-bold text-dark">{{$user->username}}</span>
+        </div>
+      </a>
+     </div>
+      <div class="col-12 col-md-8 status-photo px-0">
+        @if($status->is_nsfw && $status->media_count == 1)
+        <details class="details-animated">
+          <summary>
+            <p class="mb-0 lead font-weight-bold">CW / NSFW / Hidden Media</p>
+            <p class="font-weight-light">(click to show)</p>
+          </summary>
+          <a class="max-hide-overflow {{$status->firstMedia()->filter_class}}" href="{{$status->url()}}">
+            <img class="card-img-top" src="{{$status->mediaUrl()}}" title="{{$status->firstMedia()->caption}}" data-toggle="tooltip" data-tooltip-placement="bottom">
+          </a>
+        </details>
+        @elseif(!$status->is_nsfw && $status->media_count == 1)
+        <div class="{{$status->firstMedia()->filter_class}}">
+          <img src="{{$status->mediaUrl()}}" width="100%" title="{{$status->firstMedia()->caption}}" data-toggle="tooltip" data-placement="bottom">
+        </div>
+        @endif
+      </div>
+      @include('status.show.sidebar')
+    </div>
+  </div>
+</div>
+
+@endsection
+
+@push('meta')
+  <meta property="og:description" content="{{ $status->caption }}">
+  <meta property="og:image" content="{{$status->mediaUrl()}}">
+  <link href='{{$status->url()}}' rel='alternate' type='application/activity+json'>
+@endpush

+ 101 - 0
resources/views/status/show/sidebar.blade.php

@@ -0,0 +1,101 @@
+<div class="col-12 col-md-4 px-0 d-flex flex-column border-left border-md-left-0">
+  <div class="d-md-flex d-none align-items-center justify-content-between card-header py-3 bg-white">
+    <a href="{{$user->url()}}" class="d-flex align-items-center status-username text-truncate" data-toggle="tooltip" data-placement="bottom" title="{{$user->username}}">
+      <div class="status-avatar mr-2">
+        <img src="{{$user->avatarUrl()}}" width="24px" height="24px" style="border-radius:12px;">
+      </div>
+      <div class="username">
+        <span class="username-link font-weight-bold text-dark">{{$user->username}}</span>
+      </div>
+    </a>
+    <div class="float-right">
+      <div class="dropdown">
+        <button class="btn btn-link text-dark no-caret dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" title="Post options">
+          <span class="fas fa-ellipsis-v text-muted"></span>
+        </button>
+        <div class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownMenuButton">
+          <a class="dropdown-item font-weight-bold" href="{{$status->reportUrl()}}">Report</a>
+          {{-- <a class="dropdown-item" href="#">Embed</a> --}}
+          @if(Auth::check())
+          @if(Auth::user()->profile->id === $status->profile->id || Auth::user()->is_admin == true)
+          {{-- <a class="dropdown-item" href="{{$status->editUrl()}}">Edit</a> --}}
+          <form method="post" action="/i/delete">
+            @csrf
+            <input type="hidden" name="type" value="post">
+            <input type="hidden" name="item" value="{{$status->id}}">
+            <button type="submit" class="dropdown-item btn btn-link font-weight-bold">Delete</button>
+          </form>
+          @endif
+          @endif
+
+        </div>
+      </div>
+    </div>
+  </div>
+  <div class="d-flex flex-md-column flex-column-reverse h-100">
+    <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" v-pre>{!! $status->rendered ?? e($status->caption) !!}</span>
+        </p>
+        <p class="mb-1"><a href="{{$status->url()}}/c" class="text-muted">View all comments</a></p>
+        <div class="comments">
+          @foreach($replies as $item)
+          <p class="mb-1">
+            <span class="font-weight-bold pr-1"><bdi><a class="text-dark" href="{{$item->profile->url()}}">{{ str_limit($item->profile->username, 15)}}</a></bdi></span>
+            <span class="comment-text" v-pre>{!! $item->rendered ?? e($item->caption) !!} <a href="{{$item->url()}}" class="text-dark small font-weight-bold float-right pl-2">{{$item->created_at->diffForHumans(null, true, true ,true)}}</a></span>
+          </p>
+          @endforeach
+        </div>
+      </div>
+    </div>
+    <div class="card-body flex-grow-0 py-1">
+      <div class="reactions my-1">
+        @if(Auth::check())
+        <form class="d-inline-flex 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 border-0" type="submit" title="Like!">
+            <h3 class="m-0 {{$status->liked() ? 'fas fa-heart text-danger':'far fa-heart text-dark'}}"></h3>
+          </button>
+        </form>
+        <h3 class="far fa-comment pr-3 m-0" title="Comment"></h3>
+        <form class="d-inline-flex share-form pr-3" method="post" action="/i/share" style="display: inline;" data-id="{{$status->id}}" data-action="share" data-count="{{$status->shares_count}}">
+          @csrf
+          <input type="hidden" name="item" value="{{$status->id}}">
+          <button class="btn btn-link text-dark p-0" type="submit" title="Share">
+            <h3 class="m-0 {{$status->shared() ? 'fas fa-share-square text-primary':'far fa-share-square '}}"></h3>
+          </button>
+        </form>
+
+        @endif
+        <span class="float-right">
+          <form class="d-inline-flex " 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 border-0" type="submit" title="Save">
+              <h3 class="m-0 {{$status->bookmarked() ? 'fas fa-bookmark text-warning':'far fa-bookmark'}}"></h3>
+            </button>
+          </form>
+        </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 class="timestamp">
+        <a href="{{$status->url()}}" class="small text-muted">
+          {{$status->created_at->format('F j, Y')}}
+        </a>
+      </div>
+    </div>
+  </div>
+  <div class="card-footer bg-white sticky-md-bottom">
+    <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..." autocomplete="off">
+    </form>
+  </div>
+</div>

+ 50 - 0
resources/views/status/show/video.blade.php

@@ -0,0 +1,50 @@
+@extends('layouts.app',['title' => $user->username . " posted a photo: " . $status->likes_count . " likes, " . $status->comments_count . " comments" ])
+
+@section('content')
+
+<div class="container px-0 mt-md-4">
+  <div class="card card-md-rounded-0 status-container orientation-video">
+    <div class="row mx-0">
+    <div class="d-flex d-md-none align-items-center justify-content-between card-header bg-white w-100">
+      <a href="{{$user->url()}}" class="d-flex align-items-center status-username text-truncate" data-toggle="tooltip" data-placement="bottom" title="{{$user->username}}">
+        <div class="status-avatar mr-2">
+          <img src="{{$user->avatarUrl()}}" width="24px" height="24px" style="border-radius:12px;">
+        </div>
+        <div class="username">
+          <span class="username-link font-weight-bold text-dark">{{$user->username}}</span>
+        </div>
+      </a>
+     </div>
+      <div class="col-12 col-md-8 status-photo px-0">
+        @if($status->is_nsfw && $status->media_count == 1)
+        <details class="details-animated">
+          <summary>
+            <p class="mb-0 lead font-weight-bold">CW / NSFW / Hidden Media</p>
+            <p class="font-weight-light">(click to show)</p>
+          </summary>
+          <div class="embed-responsive embed-responsive-16by9">
+            <video class="embed-responsive-item" controls="">
+              <source src="{{$status->mediaUrl()}}" type="video/mp4">
+            </video>
+          </div>
+        </details>
+        @elseif(!$status->is_nsfw && $status->media_count == 1)
+        <div class="embed-responsive embed-responsive-16by9">
+          <video class="embed-responsive-item" controls="">
+            <source src="{{$status->mediaUrl()}}" type="video/mp4">
+          </video>
+        </div>
+        @endif
+      </div>
+      @include('status.show.sidebar')
+    </div>
+  </div>
+</div>
+
+@endsection
+
+@push('meta')
+  <meta property="og:description" content="{{ $status->caption }}">
+  <meta property="og:image" content="{{$status->mediaUrl()}}">
+  <link href='{{$status->url()}}' rel='alternate' type='application/activity+json'>
+@endpush

+ 9 - 0
routes/web.php

@@ -67,11 +67,18 @@ Route::domain(config('pixelfed.domain.app'))->middleware('validemail')->group(fu
 
     Route::group(['prefix' => 'report'], function() {
       Route::get('/', 'ReportController@showForm')->name('report.form');
+      Route::post('/', 'ReportController@formStore');
       Route::get('not-interested', 'ReportController@notInterestedForm')->name('report.not-interested');
       Route::get('spam', 'ReportController@spamForm')->name('report.spam');
       Route::get('spam/comment', 'ReportController@spamCommentForm')->name('report.spam.comment');
       Route::get('spam/post', 'ReportController@spamPostForm')->name('report.spam.post');
       Route::get('spam/profile', 'ReportController@spamProfileForm')->name('report.spam.profile');
+      Route::get('sensitive/comment', 'ReportController@sensitiveCommentForm')->name('report.sensitive.comment');
+      Route::get('sensitive/post', 'ReportController@sensitivePostForm')->name('report.sensitive.post');
+      Route::get('sensitive/profile', 'ReportController@sensitiveProfileForm')->name('report.sensitive.profile');
+      Route::get('abusive/comment', 'ReportController@abusiveCommentForm')->name('report.abusive.comment');
+      Route::get('abusive/post', 'ReportController@abusivePostForm')->name('report.abusive.post');
+      Route::get('abusive/profile', 'ReportController@abusiveProfileForm')->name('report.abusive.profile');
     });
 
   });
@@ -128,6 +135,8 @@ Route::domain(config('pixelfed.domain.app'))->middleware('validemail')->group(fu
 
   Route::get('p/{username}/{id}/c/{cid}', 'CommentController@show');
   Route::get('p/{username}/{id}/c', 'CommentController@showAll');
+  Route::get('p/{username}/{id}/edit', 'StatusController@edit');
+  Route::post('p/{username}/{id}/edit', 'StatusController@editStore');
   Route::get('p/{username}/{id}', 'StatusController@show');
   Route::get('{username}/saved', 'ProfileController@savedBookmarks');
   Route::get('{username}/followers', 'ProfileController@followers');

+ 31 - 0
tests/Unit/CryptoTest.php

@@ -0,0 +1,31 @@
+<?php
+
+namespace Tests\Unit;
+
+use Tests\TestCase;
+use Illuminate\Foundation\Testing\WithFaker;
+use Illuminate\Foundation\Testing\RefreshDatabase;
+
+class CryptoTest extends TestCase
+{
+    /**
+     * A basic test to check if PHPSecLib is installed.
+     *
+     * @return void
+     */
+    public function testLibraryInstalled()
+    {
+        $this->assertTrue(class_exists('\phpseclib\Crypt\RSA'));
+    }
+
+    public function testRSASigning()
+    {
+        $rsa = new \phpseclib\Crypt\RSA();
+        extract($rsa->createKey());
+        $rsa->loadKey($privatekey);
+        $plaintext = 'pixelfed rsa test';
+        $signature = $rsa->sign($plaintext);
+        $rsa->loadKey($publickey);
+        $this->assertTrue($rsa->verify($plaintext, $signature));
+    }
+}