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

Merge pull request #2147 from pixelfed/staging

Bug fixes
daniel 5 лет назад
Родитель
Сommit
2617b15ffd
37 измененных файлов с 593 добавлено и 69 удалено
  1. 13 0
      CHANGELOG.md
  2. 21 0
      app/Http/Controllers/AccountController.php
  3. 1 1
      app/Http/Controllers/LikeController.php
  4. 1 1
      app/Http/Controllers/StatusController.php
  5. 10 0
      app/Http/Controllers/UIKitController.php
  6. 2 0
      app/ModLog.php
  7. 4 1
      app/Profile.php
  8. 59 0
      app/Services/ActivityPubFetchService.php
  9. 2 1
      app/Transformer/Api/AccountTransformer.php
  10. 1 1
      app/Transformer/Api/StoryItemTransformer.php
  11. 21 0
      app/UIKit.php
  12. 6 3
      app/UserInvite.php
  13. 23 2
      config/purify.php
  14. 39 0
      database/migrations/2020_04_13_045435_create_uikit_table.php
  15. BIN
      public/js/app.js
  16. BIN
      public/js/profile.js
  17. BIN
      public/js/rempro.js
  18. BIN
      public/js/status.js
  19. BIN
      public/js/timeline.js
  20. BIN
      public/mix-manifest.json
  21. 25 0
      resources/assets/js/app.js
  22. 31 31
      resources/assets/js/components/NotificationCard.vue
  23. 1 23
      resources/assets/js/components/PostComponent.vue
  24. 6 1
      resources/assets/js/components/Profile.vue
  25. 34 4
      resources/assets/js/components/RemoteProfile.vue
  26. 19 0
      resources/lang/th/auth.php
  27. 11 0
      resources/lang/th/exception.php
  28. 26 0
      resources/lang/th/helpcenter.php
  29. 19 0
      resources/lang/th/navmenu.php
  30. 12 0
      resources/lang/th/notification.php
  31. 19 0
      resources/lang/th/pagination.php
  32. 22 0
      resources/lang/th/passwords.php
  33. 15 0
      resources/lang/th/profile.php
  34. 20 0
      resources/lang/th/site.php
  35. 7 0
      resources/lang/th/timeline.php
  36. 122 0
      resources/lang/th/validation.php
  37. 1 0
      routes/web.php

+ 13 - 0
CHANGELOG.md

@@ -2,7 +2,9 @@
 
 ## [Unreleased](https://github.com/pixelfed/pixelfed/compare/v0.10.9...dev)
 ### Added
+- ActivityPubFetchService for signed GET requests ([8763bfc5](https://github.com/pixelfed/pixelfed/commit/8763bfc5))
 - Custom content warnings for remote posts ([6afc61a4](https://github.com/pixelfed/pixelfed/commit/6afc61a4))
+- Thai translations ([https://github.com/pixelfed/pixelfed/commit/74cd536](https://github.com/pixelfed/pixelfed/commit/74cd536))
 
 ### Updated
 - Updated PostComponent, fix remote urls ([42716ccc](https://github.com/pixelfed/pixelfed/commit/42716ccc))
@@ -14,6 +16,17 @@
 - Updated PublicApiControllers, fix block/mutes filtering on public timeline ([08383dd4](https://github.com/pixelfed/pixelfed/commit/08383dd4))
 - Updated FixUsernames command, fixes remote username search ([0f943f67](https://github.com/pixelfed/pixelfed/commit/0f943f67))
 - Updated Timeline component, fix mod tools ([b1d5eb05](https://github.com/pixelfed/pixelfed/commit/b1d5eb05))
+- Updated Profile.vue component, fix pagination bug ([46767810](https://github.com/pixelfed/pixelfed/commit/46767810))
+- Updated purify config, fix microformats support ([877023fb](https://github.com/pixelfed/pixelfed/commit/877023fb))
+- Updated LikeController, fix likes_count bug ([996866cb](https://github.com/pixelfed/pixelfed/commit/996866cb))
+- Updated AccountController, added followRequestJson method ([483548e2](https://github.com/pixelfed/pixelfed/commit/483548e2))
+- Updated UserInvite model, added sender relation ([591a1929](https://github.com/pixelfed/pixelfed/commit/591a1929))
+- Updated migrations, added UIKit ([fcab5010](https://github.com/pixelfed/pixelfed/commit/fcab5010))
+- Updated AccountTransformer, added last_fetched_at attribute ([38b0233e](https://github.com/pixelfed/pixelfed/commit/38b0233e))
+- Updated StoryItemTransformer, increase story length to 5 seconds ([924e424c](https://github.com/pixelfed/pixelfed/commit/924e424c))
+- Updated StatusController, fix reblog_count bug ([1dc65e93](https://github.com/pixelfed/pixelfed/commit/1dc65e93))
+- Updated NotificationCard.vue component, add follow requests at top of card, remove card-header ([5e48ffca](https://github.com/pixelfed/pixelfed/commit/5e48ffca))
+- Updated RemoteProfile.vue component, add warning for empty profiles and last_fetched_at ([66f44a9d](https://github.com/pixelfed/pixelfed/commit/66f44a9d))
 
 
 ## [v0.10.9 (2020-04-17)](https://github.com/pixelfed/pixelfed/compare/v0.10.8...v0.10.9)

+ 21 - 0
app/Http/Controllers/AccountController.php

@@ -327,6 +327,27 @@ class AccountController extends Controller
 		return view('account.follow-requests', compact('followers'));
 	}
 
+	public function followRequestsJson(Request $request)
+	{
+		$pid = Auth::user()->profile_id;
+		$followers = FollowRequest::whereFollowingId($pid)->orderBy('id','desc')->whereIsRejected(0)->get();
+		$res = [
+			'count' => $followers->count(),
+			'accounts' => $followers->take(10)->map(function($a) {
+				$actor = $a->actor;
+				return [
+					'id' => $actor->id,
+					'username' => $actor->username,
+					'avatar' => $actor->avatarUrl(),
+					'url' => $actor->url(),
+					'local' => $actor->domain == null,
+					'following' => $actor->followedBy(Auth::user()->profile)
+				];
+			})
+		];
+		return response()->json($res, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
+	}
+
 	public function followRequestHandle(Request $request)
 	{
 		$this->validate($request, [

+ 1 - 1
app/Http/Controllers/LikeController.php

@@ -27,7 +27,7 @@ class LikeController extends Controller
         $profile = $user->profile;
         $status = Status::findOrFail($request->input('item'));
 
-        $count = $status->likes_count;
+        $count = $status->likes()->count();
 
         if ($status->likes()->whereProfileId($profile->id)->count() !== 0) {
             $like = Like::whereProfileId($profile->id)->whereStatusId($status->id)->firstOrFail();

+ 1 - 1
app/Http/Controllers/StatusController.php

@@ -175,7 +175,7 @@ class StatusController extends Controller
             ->whereIn('scope', ['public', 'unlisted'])
             ->findOrFail($request->input('item'));
 
-        $count = $status->shares_count;
+        $count = $status->shares()->count();
 
         $exists = Status::whereProfileId(Auth::user()->profile->id)
                   ->whereReblogOfId($status->id)

+ 10 - 0
app/Http/Controllers/UIKitController.php

@@ -0,0 +1,10 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use Illuminate\Http\Request;
+
+class UIKitController extends Controller
+{
+    //
+}

+ 2 - 0
app/ModLog.php

@@ -8,6 +8,8 @@ class ModLog extends Model
 {
 	protected $visible = ['id'];
 
+	protected $fillable = ['*'];
+
 	public function admin()
 	{
 		return $this->belongsTo(User::class, 'user_id');

+ 4 - 1
app/Profile.php

@@ -18,7 +18,10 @@ class Profile extends Model
      */
     public $incrementing = false;
     
-    protected $dates = ['deleted_at'];
+    protected $dates = [
+        'deleted_at',
+        'last_fetched_at'
+    ];
     protected $hidden = ['private_key'];
     protected $visible = ['id', 'user_id', 'username', 'name'];
     protected $fillable = ['user_id'];

+ 59 - 0
app/Services/ActivityPubFetchService.php

@@ -0,0 +1,59 @@
+<?php
+
+namespace App\Services;
+
+use Zttp\Zttp;
+use App\Profile;
+use App\Util\ActivityPub\Helpers;
+use App\Util\ActivityPub\HttpSignature;
+
+class ActivityPubFetchService
+{
+	public $signed = true;
+	public $actor;
+	public $url;
+	public $headers = [
+		'Accept'		=> 'application/activity+json, application/json',
+		'User-Agent'	=> 'PixelfedBot - https://pixelfed.org'
+	];
+
+	public static function queue()
+	{
+		return new self;
+	}
+
+	public function signed($signed = true)
+	{
+		$this->signed = $signed;
+		return $this;
+	}
+
+	public function actor($profile)
+	{
+		$this->actor = $profile;
+		return $this;
+	}
+
+	public function url($url)
+	{
+		if(!Helpers::validateUrl($url)) {
+			throw new \Exception('Invalid URL');
+		}
+		$this->url = $url;
+		return $this;
+	}
+
+	public function get()
+	{
+		if($this->signed == true && $this->actor == null) {
+			throw new \Exception('Cannot sign request without actor');
+		}
+		return $this->signedRequest();
+	}
+
+	protected function signedRequest()
+	{
+		$this->headers = HttpSignature::sign($this->actor, $this->url, false, $this->headers);
+		return Zttp::withHeaders($this->headers)->get($this->url)->body();
+	}
+}

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

@@ -34,7 +34,8 @@ class AccountTransformer extends Fractal\TransformerAbstract
 			'local' => (bool) $local,
 			'is_admin' => (bool) $is_admin,
 			'created_at' => $profile->created_at->toJSON(),
-			'header_bg' => $profile->header_bg
+			'header_bg' => $profile->header_bg,
+			'last_fetched_at' => optional($profile->last_fetched_at)->toJSON()
 		];
 	}
 

+ 1 - 1
app/Transformer/Api/StoryItemTransformer.php

@@ -14,7 +14,7 @@ class StoryItemTransformer extends Fractal\TransformerAbstract
         return [
             'id'                        => (string) $item->id,
             'type'                      => $item->type,
-            'length'                    => $item->duration != 0 ? $item->duration : 3,
+            'length'                    => 5,
             'src'                       => $item->url(),
             'preview'                   => null,
             'link'                      => null,

+ 21 - 0
app/UIKit.php

@@ -0,0 +1,21 @@
+<?php
+
+namespace App;
+
+use Illuminate\Database\Eloquent\Model;
+
+class UIKit extends Model
+{
+    protected $table = 'uikit';
+    protected $fillable = [
+    	'k',
+    	'v',
+    	'defv',
+    	'dhis'
+    ];
+
+    public static function section($k)
+    {
+    	return (new self)->where('k', $k)->first()->v;
+    }
+}

+ 6 - 3
app/UserInvite.php

@@ -6,10 +6,13 @@ use Illuminate\Database\Eloquent\Model;
 
 class UserInvite extends Model
 {
+	public function sender()
+	{
+		return $this->belongsTo(Profile::class, 'profile_id');
+	}
+
     public function url()
     {
-    	$path = '/i/invite/code';
-    	$url = url($path, [$this->key, $this->token]);
-    	return $url;
+    	return url("/i/invite/code/{$this->key}/{$this->token}");
     }
 }

+ 23 - 2
config/purify.php

@@ -68,8 +68,8 @@ return [
         */
 
         'HTML.Allowed' => env('RESTRICT_HTML_TYPES', true) ? 
-            'a[href|title|rel],p,span,br' :
-            'a[href|title|rel],p,span,strong,em,del,b,i,s,strike,h1,h2,h3,h4,h5,h6,ul,ol,li,br',
+            'a[href|title|rel|class],p[class],span[class],br' :
+            'a[href|title|rel|class],p[class],span[class],strong,em,del,b,i,s,strike,h1,h2,h3,h4,h5,h6,ul,ol,li,br',
 
 
         /*
@@ -133,6 +133,27 @@ return [
 
         'AutoFormat.RemoveEmpty' => false,
 
+        'Attr.AllowedClasses' => [
+            'h-feed',
+            'h-entry',
+            'h-cite',
+            'h-card',
+            'p-author',
+            'p-name',
+            'p-in-reply-to',
+            'p-repost-of',
+            'p-comment',
+            'u-photo',
+            'u-uid',
+            'u-url',
+            'dt-published',
+            'e-content',
+            'mention',
+            'hashtag',
+            'ellipsis',
+            'invisible'
+        ],
+
         'Attr.AllowedRel' => [
             'noreferrer',
             'noopener',

+ 39 - 0
database/migrations/2020_04_13_045435_create_uikit_table.php

@@ -0,0 +1,39 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+class CreateUikitTable extends Migration
+{
+    /**
+     * Run the migrations.
+     *
+     * @return void
+     */
+    public function up()
+    {
+        Schema::create('uikit', function (Blueprint $table) {
+            $table->bigIncrements('id');
+            $table->string('k')->unique()->index();
+            $table->text('v')->nullable();
+            $table->json('meta')->nullable();
+            // default value for rollbacks
+            $table->text('defv')->nullable();
+            // delta history
+            $table->text('dhis')->nullable();
+            $table->unsignedInteger('edit_count')->default(0)->nullable();
+            $table->timestamps();
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     *
+     * @return void
+     */
+    public function down()
+    {
+        Schema::dropIfExists('uikit');
+    }
+}

BIN
public/js/app.js


BIN
public/js/profile.js


BIN
public/js/rempro.js


BIN
public/js/status.js


BIN
public/js/timeline.js


BIN
public/mix-manifest.json


+ 25 - 0
resources/assets/js/app.js

@@ -69,6 +69,31 @@ window.App.util = {
 				return 0;
 			}
 			return new Intl.NumberFormat(locale, { notation: notation , compactDisplay: "short" }).format(count);
+		}),
+		timeAgo: (function(ts) {
+			let date = Date.parse(ts);
+			let seconds = Math.floor((new Date() - date) / 1000);
+			let interval = Math.floor(seconds / 31536000);
+			if (interval >= 1) {
+				return interval + "y";
+			}
+			interval = Math.floor(seconds / 604800);
+			if (interval >= 1) {
+				return interval + "w";
+			}
+			interval = Math.floor(seconds / 86400);
+			if (interval >= 1) {
+				return interval + "d";
+			}
+			interval = Math.floor(seconds / 3600);
+			if (interval >= 1) {
+				return interval + "h";
+			}
+			interval = Math.floor(seconds / 60);
+			if (interval >= 1) {
+				return interval + "m";
+			}
+			return Math.floor(seconds) + "s";
 		})
 	}, 
 	filters: [

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

@@ -2,19 +2,19 @@
 <div>
 	<transition name="fade">
 		<div class="card notification-card shadow-none border">
-			<div class="card-header bg-white">
-				<p class="mb-0 d-flex align-items-center justify-content-between">
-					<span data-toggle="tooltip" data-placement="bottom"><i class="fas fa-redo fa-lg text-white"></i></span>
-					<span class="small text-dark text-uppercase font-weight-bold">Alerts</span>
-					<a class="text-decoration-none text-muted" href="/account/activity"><i class="fas fa-inbox fa-lg"></i></a>
-				</p>
-			</div>
 			<div class="card-body loader text-center" style="height: 200px;">
 				<div class="spinner-border" role="status">
 					<span class="sr-only">Loading...</span>
 				</div>
 			</div>
-			<div class="card-body pt-2 px-0 py-0 contents" style="max-height: 200px; overflow-y: scroll;">
+			<div v-if="notifications.length > 0" class="card-body px-0 py-0 contents" style="max-height: 240px; overflow-y: scroll;">
+				<div v-if="profile.locked" class="media align-items-center mt-n2 px-3 py-2 border-bottom border-lighter bg-light cursor-pointer" @click="redirect('/account/follow-requests')">
+					<div class="media-body font-weight-light pt-2 small d-flex align-items-center justify-content-between">
+						<p class="mb-0 text-lighter"><i class="fas fa-cog text-light"></i></p>
+						<p class="text-center pt-1 mb-1 text-dark font-weight-bold"><strong>{{followRequests.count}}</strong> Follow Requests</p>
+						<p class="mb-0 text-lighter"><i class="fas fa-chevron-right"></i></p>
+					</div>
+				</div>
 				<div v-if="notifications.length > 0" class="media align-items-center px-3 py-2 border-bottom border-light" v-for="(n, index) in notifications">
 					<img class="mr-2 rounded-circle" style="border:1px solid #ccc" :src="n.account.avatar" alt="" width="32px" height="32px" onerror="this.onerror=null;this.src='/storage/avatars/default.png';">
 					<div class="media-body font-weight-light small">
@@ -76,11 +76,20 @@
 				notifications: {},
 				notificationCursor: 2,
 				notificationMaxId: 0,
+				profile: {
+					locked: false
+				},
+				followRequests: null
 			};
 		},
 
 		mounted() {
+			let self = this;
 			this.fetchNotifications();
+			setTimeout(function() {
+				self.profile = window._sharedData.curUser;
+				self.fetchFollowRequests();
+			}, 500);
 		},
 
 		updated() {
@@ -138,29 +147,7 @@
 			},
 
 			timeAgo(ts) {
-				let date = Date.parse(ts);
-				let seconds = Math.floor((new Date() - date) / 1000);
-				let interval = Math.floor(seconds / 31536000);
-				if (interval >= 1) {
-					return interval + "y";
-				}
-				interval = Math.floor(seconds / 604800);
-				if (interval >= 1) {
-					return interval + "w";
-				}
-				interval = Math.floor(seconds / 86400);
-				if (interval >= 1) {
-					return interval + "d";
-				}
-				interval = Math.floor(seconds / 3600);
-				if (interval >= 1) {
-					return interval + "h";
-				}
-				interval = Math.floor(seconds / 60);
-				if (interval >= 1) {
-					return interval + "m";
-				}
-				return Math.floor(seconds) + "s";
+				return window.App.util.format.timeAgo(ts);
 			},
 
 			mentionUrl(status) {
@@ -219,6 +206,19 @@
 						}
 					}
 				});
+			},
+
+			fetchFollowRequests() {
+				if(window._sharedData.curUser.locked == true) {
+					axios.get('/account/follow-requests.json')
+					.then(res => {
+						this.followRequests = res.data;
+					})
+				}
+			},
+
+			redirect(url) {
+				window.location.href = url;
 			}
 		}
 	}

+ 1 - 23
resources/assets/js/components/PostComponent.vue

@@ -1066,29 +1066,7 @@ export default {
       },
 
       timeAgo(ts) {
-        let date = Date.parse(ts);
-        let seconds = Math.floor((new Date() - date) / 1000);
-        let interval = Math.floor(seconds / 31536000);
-        if (interval >= 1) {
-          return interval + "y";
-        }
-        interval = Math.floor(seconds / 604800);
-        if (interval >= 1) {
-          return interval + "w";
-        }
-        interval = Math.floor(seconds / 86400);
-        if (interval >= 1) {
-          return interval + "d";
-        }
-        interval = Math.floor(seconds / 3600);
-        if (interval >= 1) {
-          return interval + "h";
-        }
-        interval = Math.floor(seconds / 60);
-        if (interval >= 1) {
-          return interval + "m";
-        }
-        return Math.floor(seconds) + "s";
+        return App.util.format.timeAgo(ts);
       },
 
       emojiReaction() {

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

@@ -760,8 +760,13 @@
 								self.ids.push(d.id);
 							} 
 						});
+						let max = Math.min(...this.ids);
+						if(max == this.max_id) {
+							$state.complete();
+							return;
+						}
 						this.min_id = Math.max(...this.ids);
-						this.max_id = Math.min(...this.ids);
+						this.max_id = max;
 						$state.loaded();
 						this.loading = false;
 					} else {

+ 34 - 4
resources/assets/js/components/RemoteProfile.vue

@@ -33,8 +33,7 @@
 							</span>
 						</div>
 						<p class="pl-2 h4 font-weight-bold mb-1">{{profile.display_name}}</p>
-						<p class="pl-2 font-weight-bold mb-1 text-muted">{{profile.acct}}</p>
-						<p class="pl-2 text-muted small pt-3" v-html="profile.note"></p>
+						<p class="pl-2 font-weight-bold mb-2 text-muted">{{profile.acct}}</p>
 						<p class="pl-2 text-muted small d-flex justify-content-between">
 							<span>
 								<span class="font-weight-bold text-dark">{{profile.statuses_count}}</span>
@@ -49,8 +48,10 @@
 								<span>Followers</span>
 							</span>
 						</p>
+						<p class="pl-2 text-muted small pt-2" v-html="profile.note"></p>
 					</div>
 				</div>
+				<p class="small text-lighter p-2">Last updated: <time :datetime="profile.last_fetched_at">{{timeAgo(profile.last_fetched_at, 'ago')}}</time></p>
 			</div>
 			<div class="col-12 col-md-8 pt-5">
 				<div class="row">
@@ -110,9 +111,18 @@
 						</div>
 					</div>
 
-					<!-- <div class="col-12 mt-4">
+					<div v-if="feed.length == 0" class="col-12 mb-2">
+						<div class="d-flex justify-content-center align-items-center bg-white border rounded" style="height:60vh;">
+							<div class="text-center">
+								<p class="mb-0 lead">No posts found.</p>
+								<p class="">We haven't seen any posts from this account.</p>
+							</div>
+						</div>
+					</div>
+
+					<div v-else class="col-12 mt-4">
 						<p class="text-center mb-0 px-0"><button class="btn btn-outline-primary btn-block font-weight-bold">Load More</button></p>
-					</div> -->
+					</div>
 				</div>
 			</div>
 		</div>
@@ -191,6 +201,8 @@
 				warning: false,
 				ctxMenuStatus: false,
 				ctxMenuRelationship: false,
+				fetchingRemotePosts: false,
+				showMutualFollowers: false
 			}
 		},
 
@@ -451,6 +463,24 @@
 					swal('Error', 'Something went wrong. Please try again later.', 'error');
 				});
 			},
+
+			manuallyFetchRemotePosts($event) {
+				this.fetchingRemotePosts = true;
+				event.target.blur();
+				swal(
+					'Fetching Remote Posts',
+					'Check back in a few minutes!',
+					'info'
+				);
+			},
+
+			timeAgo(ts, suffix = false) {
+				if(ts == null) {
+					return 'never';
+				}
+				suffix = suffix ? ' ' + suffix : '';
+				return App.util.format.timeAgo(ts) + suffix;
+			},
 		}
 	}
 </script>

+ 19 - 0
resources/lang/th/auth.php

@@ -0,0 +1,19 @@
+<?php
+
+return [
+
+    /*
+    |--------------------------------------------------------------------------
+    | Authentication Language Lines
+    |--------------------------------------------------------------------------
+    |
+    | The following language lines are used during authentication for various
+    | messages that we need to display to the user. You are free to modify
+    | these language lines according to your application's requirements.
+    |
+    */
+
+    'failed'   => 'ข้อมูลไม่ตรงกับบันทึกของเรา',
+    'throttle' => 'ลงชื่อเข้าหลายครั้งเกินไป โปรดลองอีกครั้งภายใน :seconds วินาที',
+
+];

+ 11 - 0
resources/lang/th/exception.php

@@ -0,0 +1,11 @@
+<?php
+
+return [
+
+	'compose' => [
+		'invalid' => [
+			'album' => 'ต้องมีอย่างรูปหรือวีดิโอเป็นอย่างน้อย',
+		],
+	],
+
+];

+ 26 - 0
resources/lang/th/helpcenter.php

@@ -0,0 +1,26 @@
+<?php
+
+return [
+
+	'helpcenter' => 'Help Center',
+	'whatsnew' => 'What\'s New',
+
+	'gettingStarted' => 'Getting Started',
+	'sharingMedia' => 'Sharing Media',
+	'profile' => 'Profile',
+	'stories' => 'Stories',
+	'hashtags' => 'Hashtags',
+	'discover' => 'Discover',
+	'directMessages' => 'Direct Messages',
+	'timelines' => 'Timelines',
+	'embed'	=> 'Embed',
+
+	'communityGuidelines' => 'Community Guidelines',
+	'whatIsTheFediverse' => 'What is the fediverse?',
+	'controllingVisibility' => 'Controlling Visibility',
+	'blockingAccounts' => 'Blocking Accounts',
+	'safetyTips' => 'Safety Tips',
+	'reportSomething' => 'Report Something',
+	'dataPolicy' => 'Data Policy'
+
+];

+ 19 - 0
resources/lang/th/navmenu.php

@@ -0,0 +1,19 @@
+<?php
+
+return [
+	'search'			=> 'ค้นหา',
+	'home'				=> 'หน้าหลัก',
+	'local'				=> 'Local',
+	'network'			=> 'Network',
+	'discover'			=> 'Discover',
+	'viewMyProfile'		=> 'ดูโพรไฟล์',
+	'myProfile'			=> 'โพรไฟล์ของฉัน',
+	'myTimeline'		=> 'ทามไลน์ของฉัน',
+	'publicTimeline'	=> 'ทามไลน์สาธารณะ',
+	'remoteFollow'		=> 'Remote Follow',
+	'settings'			=> 'ตั้งค่า',
+	'admin'				=> 'ผู้ดูแล',
+	'logout'			=> 'ออกจากระบบ',
+	'directMessages'	=> 'ส่งข้อความ',
+	'composePost'		=> 'สร้างโพสต์',
+];

+ 12 - 0
resources/lang/th/notification.php

@@ -0,0 +1,12 @@
+<?php
+
+return [
+
+  'likedPhoto'          => 'ถูกใจโพสต์ของคุณ',
+  'likedComment'        => 'ถูกใจความเห็นของคุณ',
+  'startedFollowingYou' => 'ได้ติดตามคุณแล้ว',
+  'commented'           => 'ได้แสดงความเห็นโพสต์ของคุณ',
+  'mentionedYou'        => 'พูดถึงคุณ',
+  'shared'				=> 'แชร์โพสต์ของคุณ',
+
+];

+ 19 - 0
resources/lang/th/pagination.php

@@ -0,0 +1,19 @@
+<?php
+
+return [
+
+    /*
+    |--------------------------------------------------------------------------
+    | Pagination Language Lines
+    |--------------------------------------------------------------------------
+    |
+    | The following language lines are used by the paginator library to build
+    | the simple pagination links. You are free to change them to anything
+    | you want to customize your views to better match your application.
+    |
+    */
+
+    'previous' => '« ก่อนหน้า',
+    'next' => 'ถัดไป »',
+
+];

+ 22 - 0
resources/lang/th/passwords.php

@@ -0,0 +1,22 @@
+<?php
+
+return [
+
+    /*
+    |--------------------------------------------------------------------------
+    | Password Reset Language Lines
+    |--------------------------------------------------------------------------
+    |
+    | The following language lines are the default lines which match reasons
+    | that are given by the password broker for a password update attempt
+    | has failed, such as for an invalid token or invalid new password.
+    |
+    */
+
+    'password' => 'รหัสผ่านจำเป็นต้องมีอย่างน้อยหกตัวอักษร',
+    'reset'    => 'คุณได้เปลี่ยนรหัสผ่านเรียบร้อยแล้ว!',
+    'sent'     => 'หากอีเมลของคุณอยู่ในฐานข้อมูลของเราแล้ว ในอีกไม่กี่นาทีคุณจะได้รับอีเมลเพื่อแก้ใขรหัสผ่าน หากไม่พบอีเมล โปรดไปตรวจดูที่ถังขยะ',
+    'token'    => 'ใช้รหัสผ่านนี้ไม่ได้',
+    'user'     => 'หากอีเมลของคุณอยู่ในฐานข้อมูลของเราแล้ว ในอีกไม่กี่นาทีคุณจะได้รับอีเมลเพื่อแก้ใขรหัสผ่าน หากไม่พบอีเมล โปรดไปตรวจดูที่ถังขยะ',
+
+];

+ 15 - 0
resources/lang/th/profile.php

@@ -0,0 +1,15 @@
+<?php
+
+return [
+  'emptyTimeline'         => 'ผู้ใช้นี้ยังไม่มีโพสต์อะไร!',
+  'emptyFollowers'        => 'ผู้ใช้นี้ยังไม่มีผู้ติดตาม!',
+  'emptyFollowing'        => 'ผู้ใช้นี้ยังไม่ได้ติดตามใครเลย!',
+  'emptySaved'            => 'คุณยังไม่ได้บันทึกโพสต์ใด ๆ!',
+  'savedWarning'          => 'คุณเท่านั้นที่สามารถดูโพสต์นี้ได้',
+  'privateProfileWarning' => 'บัญชีนี้เป็นบัญชีส่วนบุคคล',
+  'alreadyFollow'         => ':username ได้ติดตาม',
+  'loginToSeeProfile'     => 'ดูรูปและวีดิโอ',
+
+  'status.disabled.header' 	  => 'บัญชีไม่สามารถใช้งานได้',
+  'status.disabled.body'	  => 'เสียใจด้วยนะ บัญชีนี้ไม่สามารถใช้งานได้ โปรดลองใหม่อีกครั้ง',
+];

+ 20 - 0
resources/lang/th/site.php

@@ -0,0 +1,20 @@
+<?php
+
+return [
+
+    'about'             => 'เกี่ยวกับ',
+    'help'              => 'ช่วยเหลือ',
+    'language'          => 'ภาษา',
+    'fediverse'         => 'Fediverse',
+    'opensource'        => 'Open Source',
+    'terms'             => 'ข้อกำหนด',
+    'privacy'           => 'ความเป็นส่วนตัว',
+    'l10nWip'           => 'เรากำลังอยู่ดำเนินการแปลเป็นภาษาของคุณ',
+    'currentLocale'     => 'สถานที่ปัจจุบัน',
+    'selectLocale'      => 'เลือกภาษาที่ได้รับการสนับสนุน',
+    'contact'           => 'ติดต่อ',
+    'contact-us'        => 'ติดต่อเรา',
+    'places'            => 'สถานที่',
+    'profiles'          => 'โพรไฟล์',
+
+];

+ 7 - 0
resources/lang/th/timeline.php

@@ -0,0 +1,7 @@
+<?php
+
+return [
+
+  'emptyPersonalTimeline' => 'ทามไลน์ของคุณยังไม่มีอะไร!',
+
+];

+ 122 - 0
resources/lang/th/validation.php

@@ -0,0 +1,122 @@
+<?php
+
+return [
+
+    /*
+    |--------------------------------------------------------------------------
+    | Validation Language Lines
+    |--------------------------------------------------------------------------
+    |
+    | The following language lines contain the default error messages used by
+    | the validator class. Some of these rules have multiple versions such
+    | as the size rules. Feel free to tweak each of these messages here.
+    |
+    */
+
+    'accepted'             => 'The :attribute must be accepted.',
+    'active_url'           => 'The :attribute is not a valid URL.',
+    'after'                => 'The :attribute must be a date after :date.',
+    'after_or_equal'       => 'The :attribute must be a date after or equal to :date.',
+    'alpha'                => 'The :attribute may only contain letters.',
+    'alpha_dash'           => 'The :attribute may only contain letters, numbers, and dashes.',
+    'alpha_num'            => 'The :attribute may only contain letters and numbers.',
+    'array'                => 'The :attribute must be an array.',
+    'before'               => 'The :attribute must be a date before :date.',
+    'before_or_equal'      => 'The :attribute must be a date before or equal to :date.',
+    'between'              => [
+        'numeric' => 'The :attribute must be between :min and :max.',
+        'file'    => 'The :attribute must be between :min and :max kilobytes.',
+        'string'  => 'The :attribute must be between :min and :max characters.',
+        'array'   => 'The :attribute must have between :min and :max items.',
+    ],
+    'boolean'              => 'The :attribute field must be true or false.',
+    'confirmed'            => 'The :attribute confirmation does not match.',
+    'date'                 => 'The :attribute is not a valid date.',
+    'date_format'          => 'The :attribute does not match the format :format.',
+    'different'            => 'The :attribute and :other must be different.',
+    'digits'               => 'The :attribute must be :digits digits.',
+    'digits_between'       => 'The :attribute must be between :min and :max digits.',
+    'dimensions'           => 'The :attribute has invalid image dimensions.',
+    'distinct'             => 'The :attribute field has a duplicate value.',
+    'email'                => 'The :attribute must be a valid email address.',
+    'exists'               => 'The selected :attribute is invalid.',
+    'file'                 => 'The :attribute must be a file.',
+    'filled'               => 'The :attribute field must have a value.',
+    'image'                => 'The :attribute must be an image.',
+    'in'                   => 'The selected :attribute is invalid.',
+    'in_array'             => 'The :attribute field does not exist in :other.',
+    'integer'              => 'The :attribute must be an integer.',
+    'ip'                   => 'The :attribute must be a valid IP address.',
+    'ipv4'                 => 'The :attribute must be a valid IPv4 address.',
+    'ipv6'                 => 'The :attribute must be a valid IPv6 address.',
+    'json'                 => 'The :attribute must be a valid JSON string.',
+    'max'                  => [
+        'numeric' => 'The :attribute may not be greater than :max.',
+        'file'    => 'The :attribute may not be greater than :max kilobytes.',
+        'string'  => 'The :attribute may not be greater than :max characters.',
+        'array'   => 'The :attribute may not have more than :max items.',
+    ],
+    'mimes'                => 'The :attribute must be a file of type: :values.',
+    'mimetypes'            => 'The :attribute must be a file of type: :values.',
+    'min'                  => [
+        'numeric' => 'The :attribute must be at least :min.',
+        'file'    => 'The :attribute must be at least :min kilobytes.',
+        'string'  => 'The :attribute must be at least :min characters.',
+        'array'   => 'The :attribute must have at least :min items.',
+    ],
+    'not_in'               => 'The selected :attribute is invalid.',
+    'not_regex'            => 'The :attribute format is invalid.',
+    'numeric'              => 'The :attribute must be a number.',
+    'present'              => 'The :attribute field must be present.',
+    'regex'                => 'The :attribute format is invalid.',
+    'required'             => 'The :attribute field is required.',
+    'required_if'          => 'The :attribute field is required when :other is :value.',
+    'required_unless'      => 'The :attribute field is required unless :other is in :values.',
+    'required_with'        => 'The :attribute field is required when :values is present.',
+    'required_with_all'    => 'The :attribute field is required when :values is present.',
+    'required_without'     => 'The :attribute field is required when :values is not present.',
+    'required_without_all' => 'The :attribute field is required when none of :values are present.',
+    'same'                 => 'The :attribute and :other must match.',
+    'size'                 => [
+        'numeric' => 'The :attribute must be :size.',
+        'file'    => 'The :attribute must be :size kilobytes.',
+        'string'  => 'The :attribute must be :size characters.',
+        'array'   => 'The :attribute must contain :size items.',
+    ],
+    'string'               => 'The :attribute must be a string.',
+    'timezone'             => 'The :attribute must be a valid zone.',
+    'unique'               => 'The :attribute has already been taken.',
+    'uploaded'             => 'The :attribute failed to upload.',
+    'url'                  => 'The :attribute format is invalid.',
+
+    /*
+    |--------------------------------------------------------------------------
+    | Custom Validation Language Lines
+    |--------------------------------------------------------------------------
+    |
+    | Here you may specify custom validation messages for attributes using the
+    | convention "attribute.rule" to name the lines. This makes it quick to
+    | specify a specific custom language line for a given attribute rule.
+    |
+    */
+
+    'custom' => [
+        'attribute-name' => [
+            'rule-name' => 'custom-message',
+        ],
+    ],
+
+    /*
+    |--------------------------------------------------------------------------
+    | Custom Validation Attributes
+    |--------------------------------------------------------------------------
+    |
+    | The following language lines are used to swap attribute place-holders
+    | with something more reader friendly such as E-Mail Address instead
+    | of "email". This simply helps us make messages a little cleaner.
+    |
+    */
+
+    'attributes' => [],
+
+];

+ 1 - 0
routes/web.php

@@ -272,6 +272,7 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact
         Route::get('activity', 'AccountController@notifications')->name('notifications');
         Route::get('follow-requests', 'AccountController@followRequests')->name('follow-requests');
         Route::post('follow-requests', 'AccountController@followRequestHandle');
+        Route::get('follow-requests.json', 'AccountController@followRequestsJson');
     });
 
     Route::group(['prefix' => 'settings'], function () {