Add Pinned Posts + WebUI profile fixes (#5914)

* translate notifications

* translate  profile

* fix translate privacy

* add missing keys

* pinned posts

* fix key  settings

* fix key

Co-Authored-By: daniel <danielsupernault@gmail.com>

* Update AccountImport, improve webp support

* Update GroupSettings, add missing avatar/header deletion

* Update i18n

* Update compiled assets

* Update 2025_03_19_022553_add_pinned_columns_statuses_table.php

* Fix pinned posts implementation

* Update docker readme, closes #5909

* Update post pinning, and dispatch Notification cache warming to a job, and fix reblogged state on some endpoints

* Refactor following check

* Fix ProfileFeed bookmark, likes and shares. Closes #5879

* Update PublicApiController, use pixelfed entities for /api/pixelfed/v1/accounts/id/statuses with bookmarked state

* Update changelog

* Update compiled assets

* Update i18n

---------

Co-authored-by: Felipe Mateus <eu@felipemateus.com>
pull/5915/head^2
daniel 5 months ago committed by GitHub
parent 9e34cfe9df
commit e5c577054b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -1,9 +1,15 @@
# Release Notes
## [Unreleased](https://github.com/pixelfed/pixelfed/compare/v0.12.3...dev)
## [Unreleased](https://github.com/pixelfed/pixelfed/compare/v0.12.5...dev)
### Added
- Pinned Posts ([2f655d000](https://github.com/pixelfed/pixelfed/commit/2f655d000))
### Updates
- Update PublicApiController, use pixelfed entities for /api/pixelfed/v1/accounts/id/statuses with bookmarked state ([5ddb6d842](https://github.com/pixelfed/pixelfed/commit/5ddb6d842))
- ([](https://github.com/pixelfed/pixelfed/commit/))
## [v0.12.5 (2024-03-23)](https://github.com/pixelfed/pixelfed/compare/v0.12.5...dev)
## [v0.12.5 (2025-03-23)](https://github.com/pixelfed/pixelfed/compare/v0.12.5...dev)
### Added
- Add app register email verify resends ([dbd1e17](https://github.com/pixelfed/pixelfed/commit/dbd1e17))

File diff suppressed because it is too large Load Diff

@ -26,6 +26,7 @@ use App\Jobs\ImageOptimizePipeline\ImageOptimize;
use App\Jobs\LikePipeline\LikePipeline;
use App\Jobs\MediaPipeline\MediaDeletePipeline;
use App\Jobs\MediaPipeline\MediaSyncLicensePipeline;
use App\Jobs\NotificationPipeline\NotificationWarmUserCache;
use App\Jobs\SharePipeline\SharePipeline;
use App\Jobs\SharePipeline\UndoSharePipeline;
use App\Jobs\StatusPipeline\NewStatusPipeline;
@ -763,7 +764,8 @@ class ApiV1Controller extends Controller
'reblog_of_id',
'type',
'id',
'scope'
'scope',
'pinned_order'
)
->whereProfileId($profile['id'])
->whereNull('in_reply_to_id')
@ -2387,7 +2389,7 @@ class ApiV1Controller extends Controller
if (empty($res)) {
if (! Cache::has('pf:services:notifications:hasSynced:'.$pid)) {
Cache::put('pf:services:notifications:hasSynced:'.$pid, 1, 1209600);
NotificationService::warmCache($pid, 400, true);
NotificationWarmUserCache::dispatch($pid);
}
}
@ -4435,4 +4437,61 @@ class ApiV1Controller extends Controller
})
);
}
/**
* GET /api/v1/statuses/{id}/pin
*/
public function statusPin(Request $request, $id)
{
abort_if(! $request->user(), 403);
abort_unless($request->user()->tokenCan('write'), 403);
$user = $request->user();
$status = Status::whereScope('public')->find($id);
if (! $status) {
return $this->json(['error' => 'Record not found'], 404);
}
if ($status->profile_id != $user->profile_id) {
return $this->json(['error' => "Validation failed: Someone else's post cannot be pinned"], 422);
}
$res = StatusService::markPin($status->id);
if (! $res['success']) {
return $this->json([
'error' => $res['error'],
], 422);
}
$statusRes = StatusService::get($status->id, true, true);
$status['pinned'] = true;
return $this->json($statusRes);
}
/**
* GET /api/v1/statuses/{id}/unpin
*/
public function statusUnpin(Request $request, $id)
{
abort_if(! $request->user(), 403);
abort_unless($request->user()->tokenCan('write'), 403);
$status = Status::whereScope('public')->findOrFail($id);
$user = $request->user();
if ($status->profile_id != $user->profile_id) {
return $this->json(['error' => 'Record not found'], 404);
}
$res = StatusService::unmarkPin($status->id);
if (! $res) {
return $this->json($res, 422);
}
$status = StatusService::get($status->id, true, true);
$status['pinned'] = false;
return $this->json($status);
}
}

@ -2,46 +2,22 @@
namespace App\Http\Controllers\Api;
use Illuminate\Http\Request;
use App\Http\Controllers\{
Controller,
AvatarController
};
use Auth, Cache, Storage, URL;
use Carbon\Carbon;
use App\{
Avatar,
Like,
Media,
Notification,
Profile,
Status,
StatusArchived
};
use App\Transformer\Api\{
AccountTransformer,
NotificationTransformer,
MediaTransformer,
MediaDraftTransformer,
StatusTransformer,
StatusStatelessTransformer
};
use League\Fractal;
use App\Util\Media\Filter;
use League\Fractal\Serializer\ArraySerializer;
use League\Fractal\Pagination\IlluminatePaginatorAdapter;
use App\Avatar;
use App\Http\Controllers\AvatarController;
use App\Http\Controllers\Controller;
use App\Jobs\AvatarPipeline\AvatarOptimize;
use App\Jobs\ImageOptimizePipeline\ImageOptimize;
use App\Jobs\VideoPipeline\{
VideoOptimize,
VideoPostProcess,
VideoThumbnail
};
use App\Jobs\NotificationPipeline\NotificationWarmUserCache;
use App\Services\AccountService;
use App\Services\NotificationService;
use App\Services\MediaPathService;
use App\Services\MediaBlocklistService;
use App\Services\StatusService;
use App\Status;
use App\StatusArchived;
use App\Transformer\Api\StatusStatelessTransformer;
use Auth;
use Cache;
use Illuminate\Http\Request;
use League\Fractal;
use League\Fractal\Serializer\ArraySerializer;
class BaseApiController extends Controller
{
@ -50,47 +26,47 @@ class BaseApiController extends Controller
public function __construct()
{
// $this->middleware('auth');
$this->fractal = new Fractal\Manager();
$this->fractal->setSerializer(new ArraySerializer());
$this->fractal = new Fractal\Manager;
$this->fractal->setSerializer(new ArraySerializer);
}
public function notifications(Request $request)
{
abort_if(!$request->user(), 403);
$pid = $request->user()->profile_id;
$limit = $request->input('limit', 20);
$since = $request->input('since_id');
$min = $request->input('min_id');
$max = $request->input('max_id');
if(!$since && !$min && !$max) {
$min = 1;
}
$maxId = null;
$minId = null;
if($max) {
$res = NotificationService::getMax($pid, $max, $limit);
$ids = NotificationService::getRankedMaxId($pid, $max, $limit);
if(!empty($ids)) {
$maxId = max($ids);
$minId = min($ids);
}
} else {
$res = NotificationService::getMin($pid, $min ?? $since, $limit);
$ids = NotificationService::getRankedMinId($pid, $min ?? $since, $limit);
if(!empty($ids)) {
$maxId = max($ids);
$minId = min($ids);
}
}
if(empty($res) && !Cache::has('pf:services:notifications:hasSynced:'.$pid)) {
Cache::put('pf:services:notifications:hasSynced:'.$pid, 1, 1209600);
NotificationService::warmCache($pid, 100, true);
abort_if(! $request->user(), 403);
$pid = $request->user()->profile_id;
$limit = $request->input('limit', 20);
$since = $request->input('since_id');
$min = $request->input('min_id');
$max = $request->input('max_id');
if (! $since && ! $min && ! $max) {
$min = 1;
}
$maxId = null;
$minId = null;
if ($max) {
$res = NotificationService::getMax($pid, $max, $limit);
$ids = NotificationService::getRankedMaxId($pid, $max, $limit);
if (! empty($ids)) {
$maxId = max($ids);
$minId = min($ids);
}
} else {
$res = NotificationService::getMin($pid, $min ?? $since, $limit);
$ids = NotificationService::getRankedMinId($pid, $min ?? $since, $limit);
if (! empty($ids)) {
$maxId = max($ids);
$minId = min($ids);
}
}
if (empty($res) && ! Cache::has('pf:services:notifications:hasSynced:'.$pid)) {
Cache::put('pf:services:notifications:hasSynced:'.$pid, 1, 1209600);
NotificationWarmUserCache::dispatch($pid);
}
return response()->json($res);
@ -98,17 +74,17 @@ class BaseApiController extends Controller
public function avatarUpdate(Request $request)
{
abort_if(!$request->user(), 403);
abort_if(! $request->user(), 403);
$this->validate($request, [
'upload' => 'required|mimetypes:image/jpeg,image/jpg,image/png|max:'.config('pixelfed.max_avatar_size'),
'upload' => 'required|mimetypes:image/jpeg,image/jpg,image/png|max:'.config('pixelfed.max_avatar_size'),
]);
try {
$user = Auth::user();
$profile = $user->profile;
$file = $request->file('upload');
$path = (new AvatarController())->getPath($user, $file);
$path = (new AvatarController)->getPath($user, $file);
$dir = $path['root'];
$name = $path['name'];
$public = $path['storage'];
@ -129,13 +105,13 @@ class BaseApiController extends Controller
return response()->json([
'code' => 200,
'msg' => 'Avatar successfully updated',
'msg' => 'Avatar successfully updated',
]);
}
public function verifyCredentials(Request $request)
{
abort_if(!$request->user(), 403);
abort_if(! $request->user(), 403);
$user = $request->user();
if ($user->status != null) {
@ -143,47 +119,51 @@ class BaseApiController extends Controller
abort(403);
}
$res = AccountService::get($user->profile_id);
return response()->json($res);
}
public function accountLikes(Request $request)
{
abort_if(!$request->user(), 403);
abort_if(! $request->user(), 403);
$this->validate($request, [
'page' => 'sometimes|int|min:1|max:20',
'limit' => 'sometimes|int|min:1|max:10'
'page' => 'sometimes|int|min:1|max:20',
'limit' => 'sometimes|int|min:1|max:10',
]);
$user = $request->user();
$limit = $request->input('limit', 10);
$res = \DB::table('likes')
->whereProfileId($user->profile_id)
->latest()
->simplePaginate($limit)
->map(function($id) {
$status = StatusService::get($id->status_id, false);
$status['favourited'] = true;
return $status;
})
->filter(function($post) {
return $post && isset($post['account']);
})
->values();
->whereProfileId($user->profile_id)
->latest()
->simplePaginate($limit)
->map(function ($id) use ($user) {
$status = StatusService::get($id->status_id, false);
$status['favourited'] = true;
$status['reblogged'] = (bool) StatusService::isShared($id->status_id, $user->profile_id);
return $status;
})
->filter(function ($post) {
return $post && isset($post['account']);
})
->values();
return response()->json($res);
}
public function archive(Request $request, $id)
{
abort_if(!$request->user(), 403);
abort_if(! $request->user(), 403);
$status = Status::whereNull('in_reply_to_id')
->whereNull('reblog_of_id')
->whereProfileId($request->user()->profile_id)
->findOrFail($id);
if($status->scope === 'archived') {
if ($status->scope === 'archived') {
return [200];
}
@ -204,14 +184,14 @@ class BaseApiController extends Controller
public function unarchive(Request $request, $id)
{
abort_if(!$request->user(), 403);
abort_if(! $request->user(), 403);
$status = Status::whereNull('in_reply_to_id')
->whereNull('reblog_of_id')
->whereProfileId($request->user()->profile_id)
->findOrFail($id);
if($status->scope !== 'archived') {
if ($status->scope !== 'archived') {
return [200];
}
@ -231,16 +211,17 @@ class BaseApiController extends Controller
public function archivedPosts(Request $request)
{
abort_if(!$request->user(), 403);
abort_if(! $request->user(), 403);
$statuses = Status::whereProfileId($request->user()->profile_id)
->whereScope('archived')
->orderByDesc('id')
->simplePaginate(10);
$fractal = new Fractal\Manager();
$fractal->setSerializer(new ArraySerializer());
$resource = new Fractal\Resource\Collection($statuses, new StatusStatelessTransformer());
$fractal = new Fractal\Manager;
$fractal->setSerializer(new ArraySerializer);
$resource = new Fractal\Resource\Collection($statuses, new StatusStatelessTransformer);
return $fractal->createData($resource)->toArray();
}
}

@ -103,67 +103,95 @@ class ImportPostController extends Controller
$uid = $request->user()->id;
$pid = $request->user()->profile_id;
$successCount = 0;
$errors = [];
foreach($request->input('files') as $file) {
$media = $file['media'];
$c = collect($media);
$postHash = hash('sha256', $c->toJson());
$exts = $c->map(function($m) {
$fn = last(explode('/', $m['uri']));
return last(explode('.', $fn));
});
$postType = 'photo';
if($exts->count() > 1) {
if($exts->contains('mp4')) {
if($exts->contains('jpg', 'png')) {
$postType = 'photo:video:album';
} else {
$postType = 'video:album';
}
} else {
$postType = 'photo:album';
try {
$media = $file['media'];
$c = collect($media);
$firstUri = isset($media[0]['uri']) ? $media[0]['uri'] : '';
$postHash = hash('sha256', $c->toJson() . $firstUri);
$exists = ImportPost::where('user_id', $uid)
->where('post_hash', $postHash)
->exists();
if ($exists) {
$errors[] = "Duplicate post detected. Skipping...";
continue;
}
} else {
if(in_array($exts[0], ['jpg', 'png'])) {
$postType = 'photo';
} else if(in_array($exts[0], ['mp4'])) {
$postType = 'video';
$exts = $c->map(function($m) {
$fn = last(explode('/', $m['uri']));
return last(explode('.', $fn));
});
$postType = $this->determinePostType($exts);
$ip = new ImportPost;
$ip->user_id = $uid;
$ip->profile_id = $pid;
$ip->post_hash = $postHash;
$ip->service = 'instagram';
$ip->post_type = $postType;
$ip->media_count = $c->count();
$ip->media = $c->map(function($m) {
return [
'uri' => $m['uri'],
'title' => $this->formatHashtags($m['title'] ?? ''),
'creation_timestamp' => $m['creation_timestamp'] ?? null
];
})->toArray();
$ip->caption = $c->count() > 1 ?
$this->formatHashtags($file['title'] ?? '') :
$this->formatHashtags($ip->media[0]['title'] ?? '');
$originalFilename = last(explode('/', $ip->media[0]['uri'] ?? ''));
$ip->filename = $this->sanitizeFilename($originalFilename);
$ip->metadata = $c->map(function($m) {
return [
'uri' => $m['uri'],
'media_metadata' => isset($m['media_metadata']) ? $m['media_metadata'] : null
];
})->toArray();
$creationTimestamp = $c->count() > 1 ?
($file['creation_timestamp'] ?? null) :
($media[0]['creation_timestamp'] ?? null);
if ($creationTimestamp) {
$ip->creation_date = now()->parse($creationTimestamp);
$ip->creation_year = $ip->creation_date->format('y');
$ip->creation_month = $ip->creation_date->format('m');
$ip->creation_day = $ip->creation_date->format('d');
} else {
$ip->creation_date = now();
$ip->creation_year = now()->format('y');
$ip->creation_month = now()->format('m');
$ip->creation_day = now()->format('d');
}
}
$ip = new ImportPost;
$ip->user_id = $uid;
$ip->profile_id = $pid;
$ip->post_hash = $postHash;
$ip->service = 'instagram';
$ip->post_type = $postType;
$ip->media_count = $c->count();
$ip->media = $c->map(function($m) {
return [
'uri' => $m['uri'],
'title' => $this->formatHashtags($m['title']),
'creation_timestamp' => $m['creation_timestamp']
];
})->toArray();
$ip->caption = $c->count() > 1 ? $this->formatHashtags($file['title']) : $this->formatHashtags($ip->media[0]['title']);
$ip->filename = last(explode('/', $ip->media[0]['uri']));
$ip->metadata = $c->map(function($m) {
return [
'uri' => $m['uri'],
'media_metadata' => isset($m['media_metadata']) ? $m['media_metadata'] : null
];
})->toArray();
$ip->creation_date = $c->count() > 1 ? now()->parse($file['creation_timestamp']) : now()->parse($media[0]['creation_timestamp']);
$ip->creation_year = now()->parse($ip->creation_date)->format('y');
$ip->creation_month = now()->parse($ip->creation_date)->format('m');
$ip->creation_day = now()->parse($ip->creation_date)->format('d');
$ip->save();
ImportService::getImportedFiles($pid, true);
ImportService::getPostCount($pid, true);
$ip->save();
$successCount++;
ImportService::getImportedFiles($pid, true);
ImportService::getPostCount($pid, true);
} catch (\Exception $e) {
$errors[] = $e->getMessage();
\Log::error('Import error: ' . $e->getMessage());
continue;
}
}
return [
'msg' => 'Success'
'success' => true,
'msg' => 'Successfully imported ' . $successCount . ' posts',
'errors' => $errors
];
}
@ -173,7 +201,17 @@ class ImportPostController extends Controller
$this->checkPermissions($request);
$mimes = config('import.instagram.allow_video_posts') ? 'mimetypes:image/png,image/jpeg,video/mp4' : 'mimetypes:image/png,image/jpeg';
$allowedMimeTypes = ['image/png', 'image/jpeg'];
if (config('import.instagram.allow_image_webp') && str_contains(config_cache('pixelfed.media_types'), 'image/webp')) {
$allowedMimeTypes[] = 'image/webp';
}
if (config('import.instagram.allow_video_posts')) {
$allowedMimeTypes[] = 'video/mp4';
}
$mimes = 'mimetypes:' . implode(',', $allowedMimeTypes);
$this->validate($request, [
'file' => 'required|array|max:10',
@ -186,7 +224,12 @@ class ImportPostController extends Controller
]);
foreach($request->file('file') as $file) {
$fileName = $file->getClientOriginalName();
$extension = $file->getClientOriginalExtension();
$originalName = pathinfo($file->getClientOriginalName(), PATHINFO_FILENAME);
$safeFilename = preg_replace('/[^a-zA-Z0-9_.-]/', '_', $originalName);
$fileName = $safeFilename . '.' . $extension;
$file->storeAs('imports/' . $request->user()->id . '/', $fileName);
}
@ -197,6 +240,46 @@ class ImportPostController extends Controller
];
}
private function determinePostType($exts)
{
if ($exts->count() > 1) {
if ($exts->contains('mp4')) {
if ($exts->contains('jpg', 'png', 'webp')) {
return 'photo:video:album';
} else {
return 'video:album';
}
} else {
return 'photo:album';
}
} else {
if ($exts->isEmpty()) {
return 'photo';
}
$ext = $exts[0];
if (in_array($ext, ['jpg', 'jpeg', 'png', 'webp'])) {
return 'photo';
} else if (in_array($ext, ['mp4'])) {
return 'video';
} else {
return 'photo';
}
}
}
private function sanitizeFilename($filename)
{
$parts = explode('.', $filename);
$extension = array_pop($parts);
$originalName = implode('.', $parts);
$safeFilename = preg_replace('/[^a-zA-Z0-9_.-]/', '_', $originalName);
return $safeFilename . '.' . $extension;
}
protected function checkPermissions($request, $abortOnFail = true)
{
$user = $request->user();

@ -35,6 +35,11 @@ class PublicApiController extends Controller
$this->fractal->setSerializer(new ArraySerializer);
}
public function json($res, $code = 200, $headers = [])
{
return response()->json($res, $code, $headers, JSON_UNESCAPED_SLASHES);
}
protected function getUserData($user)
{
if (! $user) {
@ -667,10 +672,8 @@ class PublicApiController extends Controller
'only_media' => 'nullable',
'pinned' => 'nullable',
'exclude_replies' => 'nullable',
'max_id' => 'nullable|integer|min:0|max:'.PHP_INT_MAX,
'since_id' => 'nullable|integer|min:0|max:'.PHP_INT_MAX,
'min_id' => 'nullable|integer|min:0|max:'.PHP_INT_MAX,
'limit' => 'nullable|integer|min:1|max:24',
'cursor' => 'nullable',
]);
$user = $request->user();
@ -683,83 +686,198 @@ class PublicApiController extends Controller
}
$limit = $request->limit ?? 9;
$max_id = $request->max_id;
$min_id = $request->min_id;
$scope = ['photo', 'photo:album', 'video', 'video:album'];
$onlyMedia = $request->input('only_media', true);
$pinned = $request->filled('pinned') && $request->boolean('pinned') == true;
$hasCursor = $request->filled('cursor');
$visibility = $this->determineVisibility($profile, $user);
if (empty($visibility)) {
return response()->json([]);
}
$result = collect();
$remainingLimit = $limit;
if ($pinned && ! $hasCursor) {
$pinnedStatuses = Status::whereProfileId($profile['id'])
->whereNotNull('pinned_order')
->orderBy('pinned_order')
->get();
$pinnedResult = $this->processStatuses($pinnedStatuses, $user, $onlyMedia);
$result = $pinnedResult;
$remainingLimit = max(1, $limit - $pinnedResult->count());
}
$paginator = Status::whereProfileId($profile['id'])
->whereNull('in_reply_to_id')
->whereNull('reblog_of_id')
->when($pinned, function ($query) {
return $query->whereNull('pinned_order');
})
->whereIn('type', $scope)
->whereIn('scope', $visibility)
->orderByDesc('id')
->cursorPaginate($remainingLimit)
->withQueryString();
$headers = $this->generatePaginationHeaders($paginator);
$regularStatuses = $this->processStatuses($paginator->items(), $user, $onlyMedia);
$result = $result->concat($regularStatuses);
return response()->json($result, 200, $headers);
}
/**
* GET /api/pixelfed/v1/statuses/{id}/pin
*/
public function statusPin(Request $request, $id)
{
abort_if(! $request->user(), 403);
$user = $request->user();
$status = Status::whereScope('public')->find($id);
if (! $status) {
return $this->json(['error' => 'Record not found'], 404);
}
if ($status->profile_id != $user->profile_id) {
return $this->json(['error' => "Validation failed: Someone else's post cannot be pinned"], 422);
}
$res = StatusService::markPin($status->id);
if (! $res['success']) {
return $this->json([
'error' => $res['error'],
], 422);
}
$statusRes = StatusService::get($status->id, true, true);
$status['pinned'] = true;
return $this->json($statusRes);
}
/**
* GET /api/pixelfed/v1/statuses/{id}/unpin
*/
public function statusUnpin(Request $request, $id)
{
abort_if(! $request->user(), 403);
$status = Status::whereScope('public')->findOrFail($id);
$user = $request->user();
if ($status->profile_id != $user->profile_id) {
return $this->json(['error' => 'Record not found'], 404);
}
if (! $min_id && ! $max_id) {
$min_id = 1;
$res = StatusService::unmarkPin($status->id);
if (! $res) {
return $this->json($res, 422);
}
$status = StatusService::get($status->id, true, true);
$status['pinned'] = false;
return $this->json($status);
}
private function determineVisibility($profile, $user)
{
if (! $user || ! isset($user->profile_id)) {
return [];
}
if (! $profile || ! isset($profile['id'])) {
return [];
}
if ($profile['id'] == $user->profile_id) {
return ['public', 'unlisted', 'private'];
}
if ($profile['locked']) {
if (! $user) {
return response()->json([]);
return [];
}
$pid = $user->profile_id;
$following = Cache::remember('profile:following:'.$pid, now()->addMinutes(1440), function () use ($pid) {
$following = Follower::whereProfileId($pid)->pluck('following_id');
$isFollowing = FollowerService::follows($pid, $profile['id']);
return $following->push($pid)->toArray();
});
$visibility = in_array($profile['id'], $following) == true ? ['public', 'unlisted', 'private'] : [];
return $isFollowing ? ['public', 'unlisted', 'private'] : ['public'];
} else {
if ($user) {
$pid = $user->profile_id;
$following = Cache::remember('profile:following:'.$pid, now()->addMinutes(1440), function () use ($pid) {
$following = Follower::whereProfileId($pid)->pluck('following_id');
$isFollowing = FollowerService::follows($pid, $profile['id']);
return $following->push($pid)->toArray();
});
$visibility = in_array($profile['id'], $following) == true ? ['public', 'unlisted', 'private'] : ['public', 'unlisted'];
return $isFollowing ? ['public', 'unlisted', 'private'] : ['public', 'unlisted'];
} else {
$visibility = ['public', 'unlisted'];
return ['public', 'unlisted'];
}
}
$dir = $min_id ? '>' : '<';
$id = $min_id ?? $max_id;
$res = Status::whereProfileId($profile['id'])
->whereNull('in_reply_to_id')
->whereNull('reblog_of_id')
->whereIn('type', $scope)
->where('id', $dir, $id)
->whereIn('scope', $visibility)
->limit($limit)
->orderByDesc('id')
->get()
->map(function ($s) use ($user) {
try {
$status = StatusService::get($s->id, false);
if (! $status) {
return false;
}
} catch (\Exception $e) {
$status = false;
}
private function processStatuses($statuses, $user, $onlyMedia)
{
return collect($statuses)->map(function ($status) use ($user) {
try {
$mastodonStatus = StatusService::get($status->id, false);
if (! $mastodonStatus) {
return null;
}
if ($user && $status) {
$status['favourited'] = (bool) LikeService::liked($user->profile_id, $s->id);
if ($user) {
$mastodonStatus['favourited'] = (bool) LikeService::liked($user->profile_id, $status->id);
$mastodonStatus['bookmarked'] = (bool) BookmarkService::get($user->profile_id, $status->id);
$mastodonStatus['reblogged'] = (bool) StatusService::isShared($status->id, $user->profile_id);
}
return $status;
})
->filter(function ($s) use ($onlyMedia) {
if (! $s) {
return $mastodonStatus;
} catch (\Exception $e) {
return null;
}
})
->filter(function ($status) use ($onlyMedia) {
if (! $status) {
return false;
}
if ($onlyMedia) {
if (
! isset($s['media_attachments']) ||
! is_array($s['media_attachments']) ||
empty($s['media_attachments'])
) {
return false;
}
return isset($status['media_attachments']) &&
is_array($status['media_attachments']) &&
! empty($status['media_attachments']);
}
return $s;
return true;
})
->values();
}
return response()->json($res);
/**
* Generate pagination link headers from paginator
*/
private function generatePaginationHeaders($paginator)
{
$link = null;
if ($paginator->onFirstPage()) {
if ($paginator->hasMorePages()) {
$link = '<'.$paginator->nextPageUrl().'>; rel="prev"';
}
} else {
if ($paginator->previousPageUrl()) {
$link = '<'.$paginator->previousPageUrl().'>; rel="next"';
}
if ($paginator->hasMorePages()) {
$link .= ($link ? ', ' : '').'<'.$paginator->nextPageUrl().'>; rel="prev"';
}
}
return isset($link) ? ['Link' => $link] : [];
}
}

@ -0,0 +1,90 @@
<?php
namespace App\Jobs\NotificationPipeline;
use App\Services\NotificationService;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
class NotificationWarmUserCache implements ShouldBeUnique, ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
/**
* The profile ID to warm cache for.
*
* @var int
*/
public $pid;
/**
* The number of times the job may be attempted.
*
* @var int
*/
public $tries = 3;
/**
* The number of seconds to wait before retrying the job.
* This creates exponential backoff: 10s, 30s, 90s
*
* @var array
*/
public $backoff = [10, 30, 90];
/**
* The number of seconds after which the job's unique lock will be released.
*
* @var int
*/
public $uniqueFor = 3600; // 1 hour
/**
* The maximum number of unhandled exceptions to allow before failing.
*
* @var int
*/
public $maxExceptions = 2;
/**
* Create a new job instance.
*
* @param int $pid The profile ID to warm cache for
* @return void
*/
public function __construct(int $pid)
{
$this->pid = $pid;
}
/**
* Get the unique ID for the job.
*/
public function uniqueId(): string
{
return 'notifications:profile_warm_cache:'.$this->pid;
}
/**
* Execute the job.
*/
public function handle(): void
{
try {
NotificationService::warmCache($this->pid, 100, true);
} catch (\Exception $e) {
Log::error('Failed to warm notification cache', [
'profile_id' => $this->pid,
'exception' => get_class($e),
'message' => $e->getMessage(),
'attempt' => $this->attempts(),
]);
throw $e;
}
}
}

@ -12,6 +12,8 @@ class StatusService
{
const CACHE_KEY = 'pf:services:status:v1.1:';
const MAX_PINNED = 3;
public static function key($id, $publicOnly = true)
{
$p = $publicOnly ? 'pub:' : 'all:';
@ -82,7 +84,6 @@ class StatusService
$status['shortcode'],
$status['taggedPeople'],
$status['thread'],
$status['pinned'],
$status['account']['header_bg'],
$status['account']['is_admin'],
$status['account']['last_fetched_at'],
@ -198,4 +199,89 @@ class StatusService
{
return InstanceService::totalLocalStatuses();
}
public static function isPinned($id)
{
return Status::whereId($id)->whereNotNull('pinned_order')->exists();
}
public static function totalPins($pid)
{
return Status::whereProfileId($pid)->whereNotNull('pinned_order')->count();
}
public static function markPin($id)
{
$status = Status::find($id);
if (! $status) {
return [
'success' => false,
'error' => 'Record not found',
];
}
if ($status->scope != 'public') {
return [
'success' => false,
'error' => 'Validation failed: you can only pin public posts',
];
}
if (self::isPinned($id)) {
return [
'success' => false,
'error' => 'This post is already pinned',
];
}
$totalPins = self::totalPins($status->profile_id);
if ($totalPins >= self::MAX_PINNED) {
return [
'success' => false,
'error' => 'Validation failed: You have already pinned the max number of posts',
];
}
$status->pinned_order = $totalPins + 1;
$status->save();
self::refresh($id);
return [
'success' => true,
'error' => null,
];
}
public static function unmarkPin($id)
{
$status = Status::find($id);
if (! $status || is_null($status->pinned_order)) {
return false;
}
$removedOrder = $status->pinned_order;
$profileId = $status->profile_id;
$status->pinned_order = null;
$status->save();
Status::where('profile_id', $profileId)
->whereNotNull('pinned_order')
->where('pinned_order', '>', $removedOrder)
->orderBy('pinned_order', 'asc')
->chunk(10, function ($statuses) {
foreach ($statuses as $s) {
$s->pinned_order = $s->pinned_order - 1;
$s->save();
}
});
self::refresh($id);
return true;
}
}

@ -69,6 +69,7 @@ class StatusStatelessTransformer extends Fractal\TransformerAbstract
'tags' => StatusHashtagService::statusTags($status->id),
'poll' => $poll,
'edited_at' => $status->edited_at ? str_replace('+00:00', 'Z', $status->edited_at->format(DATE_RFC3339_EXTENDED)) : null,
'pinned' => (bool) $status->pinned_order,
];
}
}

@ -71,6 +71,7 @@ class StatusTransformer extends Fractal\TransformerAbstract
'poll' => $poll,
'bookmarked' => BookmarkService::get($pid, $status->id),
'edited_at' => $status->edited_at ? str_replace('+00:00', 'Z', $status->edited_at->format(DATE_RFC3339_EXTENDED)) : null,
'pinned' => (bool) $status->pinned_order,
];
}
}

@ -0,0 +1,30 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('statuses', function (Blueprint $table) {
$table->tinyInteger('pinned_order')->nullable()->default(null)->index();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
//
Schema::table('statuses', function (Blueprint $table) {
$table->dropColumn('pinned_order');
});
}
};

@ -1,5 +1,5 @@
# Pixelfed + Docker + Docker Compose
Please see the [Pixelfed Docs (Next)](https://jippi.github.io/pixelfed-docs-next/pr-preview/pr-1/running-pixelfed/) for current documentation on Docker usage.
Please see the [Pixelfed Docs (Next)](https://jippi.github.io/docker-pixelfed/) for current documentation on Docker usage.
The docs can be [reviewed in the pixelfed/docs-next](https://github.com/pixelfed/docs-next/pull/1) repository.

File diff suppressed because one or more lines are too long

2
public/js/app.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -1 +1 @@
(()=>{"use strict";var e,r,o,a={},t={};function n(e){var r=t[e];if(void 0!==r)return r.exports;var o=t[e]={id:e,loaded:!1,exports:{}};return a[e].call(o.exports,o,o.exports,n),o.loaded=!0,o.exports}n.m=a,e=[],n.O=(r,o,a,t)=>{if(!o){var d=1/0;for(f=0;f<e.length;f++){for(var[o,a,t]=e[f],s=!0,c=0;c<o.length;c++)(!1&t||d>=t)&&Object.keys(n.O).every((e=>n.O[e](o[c])))?o.splice(c--,1):(s=!1,t<d&&(d=t));if(s){e.splice(f--,1);var i=a();void 0!==i&&(r=i)}}return r}t=t||0;for(var f=e.length;f>0&&e[f-1][2]>t;f--)e[f]=e[f-1];e[f]=[o,a,t]},n.n=e=>{var r=e&&e.__esModule?()=>e.default:()=>e;return n.d(r,{a:r}),r},n.d=(e,r)=>{for(var o in r)n.o(r,o)&&!n.o(e,o)&&Object.defineProperty(e,o,{enumerable:!0,get:r[o]})},n.f={},n.e=e=>Promise.all(Object.keys(n.f).reduce(((r,o)=>(n.f[o](e,r),r)),[])),n.u=e=>"js/"+{529:"groups-page",1179:"daci.chunk",1240:"discover~myhashtags.chunk",1645:"profile~following.bundle",2156:"dms.chunk",2822:"group.create",2966:"discover~hashtag.bundle",3688:"discover~serverfeed.chunk",4951:"home.chunk",6250:"discover~settings.chunk",6438:"groups-page-media",6535:"discover.chunk",6740:"discover~memories.chunk",6791:"groups-page-members",7206:"groups-page-topics",7342:"groups-post",7399:"dms~message.chunk",7413:"error404.bundle",7521:"discover~findfriends.chunk",7744:"notifications.chunk",8087:"profile.chunk",8119:"i18n.bundle",8257:"groups-page-about",8408:"post.chunk",8977:"profile~followers.bundle",9124:"compose.chunk",9231:"groups-profile",9919:"changelog.bundle"}[e]+"."+{529:"4a77f2a4e0024224",1179:"8cf1cb07ac8a9100",1240:"f4257bc65189fde3",1645:"8ebe39a19638db1b",2156:"13449036a5b769e6",2822:"38102523ebf4cde9",2966:"c8eb86fb63ede45e",3688:"4e135dd1c07c17dd",4951:"3d9801a7722f4dfb",6250:"295935b63f9c0971",6438:"526b66b27a0bd091",6535:"0ca404627af971f2",6740:"9621c5ecf4482f0a",6791:"c59de89c3b8e3a02",7206:"d279a2438ee20311",7342:"e160e406bdb4a1b0",7399:"f0d6ccb6f2f1cbf7",7413:"f5958c1713b4ab7c",7521:"bf787612b58e5473",7744:"bd37ed834e650fd7",8087:"239231da0003f8d9",8119:"85976a3b9d6b922a",8257:"16d96a32748daa93",8408:"c699382772550b42",8977:"9d2008cfa13a6f17",9124:"80e32f21442c8a91",9231:"58b5bf1af4d0722e",9919:"efd3d17aee17020e"}[e]+".js",n.miniCssF=e=>({2305:"css/portfolio",2540:"css/landing",3364:"css/admin",4370:"css/profile",6952:"css/appdark",8252:"css/app",8759:"css/spa"}[e]+".css"),n.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),n.o=(e,r)=>Object.prototype.hasOwnProperty.call(e,r),r={},o="pixelfed:",n.l=(e,a,t,d)=>{if(r[e])r[e].push(a);else{var s,c;if(void 0!==t)for(var i=document.getElementsByTagName("script"),f=0;f<i.length;f++){var u=i[f];if(u.getAttribute("src")==e||u.getAttribute("data-webpack")==o+t){s=u;break}}s||(c=!0,(s=document.createElement("script")).charset="utf-8",s.timeout=120,n.nc&&s.setAttribute("nonce",n.nc),s.setAttribute("data-webpack",o+t),s.src=e),r[e]=[a];var l=(o,a)=>{s.onerror=s.onload=null,clearTimeout(p);var t=r[e];if(delete r[e],s.parentNode&&s.parentNode.removeChild(s),t&&t.forEach((e=>e(a))),o)return o(a)},p=setTimeout(l.bind(null,void 0,{type:"timeout",target:s}),12e4);s.onerror=l.bind(null,s.onerror),s.onload=l.bind(null,s.onload),c&&document.head.appendChild(s)}},n.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.nmd=e=>(e.paths=[],e.children||(e.children=[]),e),n.p="/",(()=>{var e={461:0,6952:0,8252:0,2305:0,3364:0,2540:0,4370:0,8759:0};n.f.j=(r,o)=>{var a=n.o(e,r)?e[r]:void 0;if(0!==a)if(a)o.push(a[2]);else if(/^((69|82)52|2305|2540|3364|4370|461|8759)$/.test(r))e[r]=0;else{var t=new Promise(((o,t)=>a=e[r]=[o,t]));o.push(a[2]=t);var d=n.p+n.u(r),s=new Error;n.l(d,(o=>{if(n.o(e,r)&&(0!==(a=e[r])&&(e[r]=void 0),a)){var t=o&&("load"===o.type?"missing":o.type),d=o&&o.target&&o.target.src;s.message="Loading chunk "+r+" failed.\n("+t+": "+d+")",s.name="ChunkLoadError",s.type=t,s.request=d,a[1](s)}}),"chunk-"+r,r)}},n.O.j=r=>0===e[r];var r=(r,o)=>{var a,t,[d,s,c]=o,i=0;if(d.some((r=>0!==e[r]))){for(a in s)n.o(s,a)&&(n.m[a]=s[a]);if(c)var f=c(n)}for(r&&r(o);i<d.length;i++)t=d[i],n.o(e,t)&&e[t]&&e[t][0](),e[t]=0;return n.O(f)},o=self.webpackChunkpixelfed=self.webpackChunkpixelfed||[];o.forEach(r.bind(null,0)),o.push=r.bind(null,o.push.bind(o))})(),n.nc=void 0})();
(()=>{"use strict";var e,r,a,o={},t={};function n(e){var r=t[e];if(void 0!==r)return r.exports;var a=t[e]={id:e,loaded:!1,exports:{}};return o[e].call(a.exports,a,a.exports,n),a.loaded=!0,a.exports}n.m=o,e=[],n.O=(r,a,o,t)=>{if(!a){var c=1/0;for(f=0;f<e.length;f++){for(var[a,o,t]=e[f],d=!0,s=0;s<a.length;s++)(!1&t||c>=t)&&Object.keys(n.O).every((e=>n.O[e](a[s])))?a.splice(s--,1):(d=!1,t<c&&(c=t));if(d){e.splice(f--,1);var i=o();void 0!==i&&(r=i)}}return r}t=t||0;for(var f=e.length;f>0&&e[f-1][2]>t;f--)e[f]=e[f-1];e[f]=[a,o,t]},n.n=e=>{var r=e&&e.__esModule?()=>e.default:()=>e;return n.d(r,{a:r}),r},n.d=(e,r)=>{for(var a in r)n.o(r,a)&&!n.o(e,a)&&Object.defineProperty(e,a,{enumerable:!0,get:r[a]})},n.f={},n.e=e=>Promise.all(Object.keys(n.f).reduce(((r,a)=>(n.f[a](e,r),r)),[])),n.u=e=>"js/"+{529:"groups-page",1179:"daci.chunk",1240:"discover~myhashtags.chunk",1645:"profile~following.bundle",2156:"dms.chunk",2822:"group.create",2966:"discover~hashtag.bundle",3688:"discover~serverfeed.chunk",4951:"home.chunk",6250:"discover~settings.chunk",6438:"groups-page-media",6535:"discover.chunk",6740:"discover~memories.chunk",6791:"groups-page-members",7206:"groups-page-topics",7342:"groups-post",7399:"dms~message.chunk",7413:"error404.bundle",7521:"discover~findfriends.chunk",7744:"notifications.chunk",8087:"profile.chunk",8119:"i18n.bundle",8257:"groups-page-about",8408:"post.chunk",8977:"profile~followers.bundle",9124:"compose.chunk",9231:"groups-profile",9919:"changelog.bundle"}[e]+"."+{529:"4a77f2a4e0024224",1179:"8cf1cb07ac8a9100",1240:"03a9fc477579fd24",1645:"8ebe39a19638db1b",2156:"13449036a5b769e6",2822:"38102523ebf4cde9",2966:"b783a54ac20f3e93",3688:"4e135dd1c07c17dd",4951:"fec949c588d3a0ec",6250:"295935b63f9c0971",6438:"526b66b27a0bd091",6535:"0ca404627af971f2",6740:"9621c5ecf4482f0a",6791:"c59de89c3b8e3a02",7206:"d279a2438ee20311",7342:"e160e406bdb4a1b0",7399:"f0d6ccb6f2f1cbf7",7413:"f5958c1713b4ab7c",7521:"bf787612b58e5473",7744:"a755ad4eb2972fbf",8087:"25876d18c9eeb7c6",8119:"85976a3b9d6b922a",8257:"16d96a32748daa93",8408:"48fdffa21ac83f3a",8977:"9d2008cfa13a6f17",9124:"80e32f21442c8a91",9231:"58b5bf1af4d0722e",9919:"efd3d17aee17020e"}[e]+".js",n.miniCssF=e=>({2305:"css/portfolio",2540:"css/landing",3364:"css/admin",4370:"css/profile",6952:"css/appdark",8252:"css/app",8759:"css/spa"}[e]+".css"),n.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),n.o=(e,r)=>Object.prototype.hasOwnProperty.call(e,r),r={},a="pixelfed:",n.l=(e,o,t,c)=>{if(r[e])r[e].push(o);else{var d,s;if(void 0!==t)for(var i=document.getElementsByTagName("script"),f=0;f<i.length;f++){var u=i[f];if(u.getAttribute("src")==e||u.getAttribute("data-webpack")==a+t){d=u;break}}d||(s=!0,(d=document.createElement("script")).charset="utf-8",d.timeout=120,n.nc&&d.setAttribute("nonce",n.nc),d.setAttribute("data-webpack",a+t),d.src=e),r[e]=[o];var l=(a,o)=>{d.onerror=d.onload=null,clearTimeout(p);var t=r[e];if(delete r[e],d.parentNode&&d.parentNode.removeChild(d),t&&t.forEach((e=>e(o))),a)return a(o)},p=setTimeout(l.bind(null,void 0,{type:"timeout",target:d}),12e4);d.onerror=l.bind(null,d.onerror),d.onload=l.bind(null,d.onload),s&&document.head.appendChild(d)}},n.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.nmd=e=>(e.paths=[],e.children||(e.children=[]),e),n.p="/",(()=>{var e={461:0,6952:0,8252:0,2305:0,3364:0,2540:0,4370:0,8759:0};n.f.j=(r,a)=>{var o=n.o(e,r)?e[r]:void 0;if(0!==o)if(o)a.push(o[2]);else if(/^((69|82)52|2305|2540|3364|4370|461|8759)$/.test(r))e[r]=0;else{var t=new Promise(((a,t)=>o=e[r]=[a,t]));a.push(o[2]=t);var c=n.p+n.u(r),d=new Error;n.l(c,(a=>{if(n.o(e,r)&&(0!==(o=e[r])&&(e[r]=void 0),o)){var t=a&&("load"===a.type?"missing":a.type),c=a&&a.target&&a.target.src;d.message="Loading chunk "+r+" failed.\n("+t+": "+c+")",d.name="ChunkLoadError",d.type=t,d.request=c,o[1](d)}}),"chunk-"+r,r)}},n.O.j=r=>0===e[r];var r=(r,a)=>{var o,t,[c,d,s]=a,i=0;if(c.some((r=>0!==e[r]))){for(o in d)n.o(d,o)&&(n.m[o]=d[o]);if(s)var f=s(n)}for(r&&r(a);i<c.length;i++)t=c[i],n.o(e,t)&&e[t]&&e[t][0](),e[t]=0;return n.O(f)},a=self.webpackChunkpixelfed=self.webpackChunkpixelfed||[];a.forEach(r.bind(null,0)),a.push=r.bind(null,a.push.bind(a))})(),n.nc=void 0})();

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

2
public/js/spa.js vendored

File diff suppressed because one or more lines are too long

@ -1,9 +1,9 @@
{
"/js/app.js": "/js/app.js?id=2363ff0a931b91ef2d6d21a6dfadab4a",
"/js/app.js": "/js/app.js?id=01640f9df96d57fa07a677a851e07934",
"/js/activity.js": "/js/activity.js?id=a7e66fc4edffd7ac88264ec77ecc897f",
"/js/components.js": "/js/components.js?id=9b6f094bb7d0e43a737ed1d1756f8653",
"/js/discover.js": "/js/discover.js?id=0a7264152a6fcef7a5b2a2fc5775c86c",
"/js/profile.js": "/js/profile.js?id=f2acd89e5c6d2ee00804f7bb752b30bc",
"/js/profile.js": "/js/profile.js?id=a7c51437b153c91f919857dbcbec1d68",
"/js/status.js": "/js/status.js?id=07bbfc11596b0043f7032b616793dd64",
"/js/timeline.js": "/js/timeline.js?id=e2aa9a18c2b16cfae47a8b0cfd0765f2",
"/js/compose.js": "/js/compose.js?id=85045be7b0a762894ea4d1fff00fd809",
@ -17,34 +17,34 @@
"/js/story-compose.js": "/js/story-compose.js?id=a25351b1487264fd49458d47cd8c121f",
"/js/direct.js": "/js/direct.js?id=3e9c970e8ee5cc4e744a262b6b58339a",
"/js/admin.js": "/js/admin.js?id=4f07b4fac37aa56cf7db83f76a20d0d6",
"/js/spa.js": "/js/spa.js?id=c0bef31682e3e261bf019c1bfb429c6f",
"/js/spa.js": "/js/spa.js?id=d2af4db6988b0ef161b89809d6f41a33",
"/js/stories.js": "/js/stories.js?id=f5637cea14c47edfa96df7346b724236",
"/js/portfolio.js": "/js/portfolio.js?id=5f64242a8cccdeb9d0642c9216396192",
"/js/account-import.js": "/js/account-import.js?id=e1715ad431edffa5b0533f269743af2e",
"/js/account-import.js": "/js/account-import.js?id=910fa8ccd6563f4711fa4214b00e898e",
"/js/admin_invite.js": "/js/admin_invite.js?id=883bf2f76521fc6b31eb1a3d4fb915ff",
"/js/landing.js": "/js/landing.js?id=17b9aa7ce308ee5f09c96f29fa8f2c47",
"/js/landing.js": "/js/landing.js?id=d3b87b502df845bcb2d70fd57c763959",
"/js/remote_auth.js": "/js/remote_auth.js?id=1a952303a4e5c6651960a3b92b9f5134",
"/js/groups.js": "/js/groups.js?id=9728981e8c4a6c7bf52c6695aa3e3b2a",
"/js/groups.js": "/js/groups.js?id=31ff019e974862dc0e565fbc3209166f",
"/js/group-status.js": "/js/group-status.js?id=c5a4b95b4b180f70fa10e01760f8c999",
"/js/group-topic-feed.js": "/js/group-topic-feed.js?id=587c552bb4d1a9f329ac5ed4a5827e61",
"/js/manifest.js": "/js/manifest.js?id=d57720236163c3aa1414d071dd7e4ff4",
"/js/home.chunk.3d9801a7722f4dfb.js": "/js/home.chunk.3d9801a7722f4dfb.js?id=248276b7039aa622ea3882c13fdbac04",
"/js/manifest.js": "/js/manifest.js?id=75d6e5832987e8e354fa46a56dc6aee5",
"/js/home.chunk.fec949c588d3a0ec.js": "/js/home.chunk.fec949c588d3a0ec.js?id=8c72419c75a578f6819324d508f23b4f",
"/js/compose.chunk.80e32f21442c8a91.js": "/js/compose.chunk.80e32f21442c8a91.js?id=c27c7ab6f212ffbdbf58f532133ef610",
"/js/post.chunk.c699382772550b42.js": "/js/post.chunk.c699382772550b42.js?id=8439251d319cc91821c534baccb06981",
"/js/profile.chunk.239231da0003f8d9.js": "/js/profile.chunk.239231da0003f8d9.js?id=1da396dfe03006429d8e422fa298896b",
"/js/post.chunk.48fdffa21ac83f3a.js": "/js/post.chunk.48fdffa21ac83f3a.js?id=a112cbd4be2d1f768f1410865f3b16e8",
"/js/profile.chunk.25876d18c9eeb7c6.js": "/js/profile.chunk.25876d18c9eeb7c6.js?id=c4c0ef1586def4185037ff1d05c4a18e",
"/js/discover~memories.chunk.9621c5ecf4482f0a.js": "/js/discover~memories.chunk.9621c5ecf4482f0a.js?id=55e3a3786066d7e8cecc3a66a7332960",
"/js/discover~myhashtags.chunk.f4257bc65189fde3.js": "/js/discover~myhashtags.chunk.f4257bc65189fde3.js?id=9f2da3290e56aab976dd7c8e8b14f1cd",
"/js/discover~myhashtags.chunk.03a9fc477579fd24.js": "/js/discover~myhashtags.chunk.03a9fc477579fd24.js?id=76b01eea69a257b36cb046cedd1cdfba",
"/js/daci.chunk.8cf1cb07ac8a9100.js": "/js/daci.chunk.8cf1cb07ac8a9100.js?id=e6da2e8e435ba11f28cc14050ba2ec4e",
"/js/discover~findfriends.chunk.bf787612b58e5473.js": "/js/discover~findfriends.chunk.bf787612b58e5473.js?id=76eb7c87adb60409a4831ff975e6c3f4",
"/js/discover~serverfeed.chunk.4e135dd1c07c17dd.js": "/js/discover~serverfeed.chunk.4e135dd1c07c17dd.js?id=52ce6ef8ca5b08628df6530abbab8d2a",
"/js/discover~settings.chunk.295935b63f9c0971.js": "/js/discover~settings.chunk.295935b63f9c0971.js?id=b74753937401ff97936daed7af0aa47f",
"/js/discover.chunk.0ca404627af971f2.js": "/js/discover.chunk.0ca404627af971f2.js?id=fb662f204f0a3d50ce8e7ee65f5499d1",
"/js/notifications.chunk.bd37ed834e650fd7.js": "/js/notifications.chunk.bd37ed834e650fd7.js?id=ecd148f74f4b559bf7783b4ac2032454",
"/js/notifications.chunk.a755ad4eb2972fbf.js": "/js/notifications.chunk.a755ad4eb2972fbf.js?id=04421d6fe4bae55dd2d044eecc41eb14",
"/js/dms.chunk.13449036a5b769e6.js": "/js/dms.chunk.13449036a5b769e6.js?id=e78688a49ad274ca3bc4cc7bc54a20c4",
"/js/dms~message.chunk.f0d6ccb6f2f1cbf7.js": "/js/dms~message.chunk.f0d6ccb6f2f1cbf7.js?id=e130002bd287f084ffca6de9dd758e9d",
"/js/profile~followers.bundle.9d2008cfa13a6f17.js": "/js/profile~followers.bundle.9d2008cfa13a6f17.js?id=6e9c0c2c42d55c4c3db48aacda336e69",
"/js/profile~following.bundle.8ebe39a19638db1b.js": "/js/profile~following.bundle.8ebe39a19638db1b.js?id=239a879240723ec8cef74958f10167e9",
"/js/discover~hashtag.bundle.c8eb86fb63ede45e.js": "/js/discover~hashtag.bundle.c8eb86fb63ede45e.js?id=5ff71e7570d90c396d6ece0d0ea40c60",
"/js/discover~hashtag.bundle.b783a54ac20f3e93.js": "/js/discover~hashtag.bundle.b783a54ac20f3e93.js?id=37846a632e2e99b6b401a6b9f6b4354a",
"/js/error404.bundle.f5958c1713b4ab7c.js": "/js/error404.bundle.f5958c1713b4ab7c.js?id=0dc878fd60f73c85280b293b6d6c091a",
"/js/i18n.bundle.85976a3b9d6b922a.js": "/js/i18n.bundle.85976a3b9d6b922a.js?id=62e1a930a6b89be0b6a72613ec578fb4",
"/js/changelog.bundle.efd3d17aee17020e.js": "/js/changelog.bundle.efd3d17aee17020e.js?id=777875be1b3bf4d1520aafc55e71c4c4",

@ -367,7 +367,7 @@
},
async filterPostMeta(media) {
let fbfix = await this.fixFacebookEncoding(media);
let fbfix = await this.fixFacebookEncoding(media);
let json = JSON.parse(fbfix);
/* Sometimes the JSON isn't an array, when there's only one post */
if (!Array.isArray(json)) {
@ -422,24 +422,32 @@
this.filterPostMeta(media);
let imgs = await Promise.all(entries.filter(entry => {
return (entry.filename.startsWith('media/posts/') || entry.filename.startsWith('media/other/')) && (entry.filename.endsWith('.png') || entry.filename.endsWith('.jpg') || entry.filename.endsWith('.mp4'));
const supportedFormats = ['.png', '.jpg', '.jpeg', '.mp4'];
if (this.config.allow_image_webp) {
supportedFormats.push('.webp');
}
return (entry.filename.startsWith('media/posts/') || entry.filename.startsWith('media/other/')) &&
supportedFormats.some(format => entry.filename.endsWith(format));
})
.map(async entry => {
const supportedFormats = ['.png', '.jpg', '.jpeg', '.mp4'];
if (this.config.allow_image_webp) {
supportedFormats.push('.webp');
}
if(
(
entry.filename.startsWith('media/posts/') ||
entry.filename.startsWith('media/other/')
) && (
entry.filename.endsWith('.png') ||
entry.filename.endsWith('.jpg') ||
entry.filename.endsWith('.mp4')
)
) &&
supportedFormats.some(format => entry.filename.endsWith(format))
) {
let types = {
'png': 'image/png',
'jpg': 'image/jpeg',
'jpeg': 'image/jpeg',
'mp4': 'video/mp4'
'mp4': 'video/mp4',
'webp': 'image/webp'
}
let type = types[entry.filename.split('/').pop().split('.').pop()];
let blob = await entry.getData(new zip.BlobWriter(type));
@ -517,6 +525,15 @@
return res;
},
getFilename(filename) {
const baseName = filename.split('/').pop();
const extension = baseName.split('.').pop();
const originalName = baseName.substring(0, baseName.lastIndexOf('.'));
const updatedFilename = originalName.replace(/[^a-zA-Z0-9_.-]/g, '_');
return updatedFilename + '.' + extension;
},
handleImport() {
swal('Importing...', "Please wait while we upload your imported posts.\n Keep this page open and do not navigate away.", 'success');
this.importButtonLoading = true;
@ -527,8 +544,9 @@
chunks.forEach(c => {
let formData = new FormData();
c.map((e, idx) => {
let file = new File([e.file], e.filename);
formData.append('file['+ idx +']', file, e.filename.split('/').pop());
let chunkedFilename = this.getFilename(e.filename);
let file = new File([e.file], chunkedFilename);
formData.append('file['+ idx +']', file, chunkedFilename);
})
axios.post(
'/api/local/import/ig/media',

@ -9,7 +9,7 @@
<div class="col-md-9 col-lg-9 col-xl-5 offset-xl-1">
<template v-if="tabIndex === 0">
<h1 class="font-weight-bold">
Notifications
{{ $t("notifications.title")}}
</h1>
<p class="small mt-n2">&nbsp;</p>
</template>
@ -19,7 +19,7 @@
<i class="far fa-chevron-circle-left fa-2x mr-3" title="Go back to notifications"></i>
</a>
<h1 class="font-weight-bold">
Follow Requests
{{ $t("notifications.followRequest") }}
</h1>
</div>
</template>
@ -141,13 +141,13 @@
class="btn btn-outline-success py-1 btn-sm font-weight-bold rounded-pill mr-2 mb-1"
@click.prevent="handleFollowRequest('accept', index)"
>
Accept
{{ $t('notifications.accept') }}
</button>
<button class="btn btn-outline-lighter py-1 btn-sm font-weight-bold rounded-pill mb-1"
@click.prevent="handleFollowRequest('reject', index)"
>
Reject
{{ $t("notifications.reject") }}
</button>
</div>
</div>
@ -161,7 +161,7 @@
<div class="media align-items-center small">
<i class="far fa-exclamation-triangle mx-2"></i>
<div class="media-body">
<p class="mb-0 font-weight-bold">Filtering results may not include older notifications</p>
<p class="mb-0 font-weight-bold">{{ $t("notifications.filteringResults") }}</p>
</div>
</div>
</div>
@ -244,40 +244,40 @@
{
id: 'mentions',
name: 'Mentions',
description: 'Replies to your posts and posts you were mentioned in',
name: this.$t("notifications.mentions"),
description: this.$t("notifications.mentionsDescription"),
icon: 'far fa-at',
types: ['comment', 'mention']
},
{
id: 'likes',
name: 'Likes',
description: 'Accounts that liked your posts',
name: this.$t("notifications.likes"),
description: this.$t("notifications.likesDescription"),
icon: 'far fa-heart',
types: ['favourite']
},
{
id: 'followers',
name: 'Followers',
description: 'Accounts that followed you',
name: this.$t("notifications.followers"),
description: this.$t("notifications.followersDescription"),
icon: 'far fa-user-plus',
types: ['follow']
},
{
id: 'reblogs',
name: 'Reblogs',
description: 'Accounts that shared or reblogged your posts',
name: this.$t("notifications.reblogs"),
description:this.$t("notifications.reblogsDescription"),
icon: 'far fa-retweet',
types: ['share']
},
{
id: 'direct',
name: 'DMs',
description: 'Direct messages you have with other accounts',
name: this.$t("notifications.dms"),
description: this.$t("notifications.dmsDescription"),
icon: 'far fa-envelope',
types: ['direct']
},

@ -85,6 +85,8 @@
:profile="user"
@report-modal="handleReport()"
@delete="deletePost()"
@pinned="handlePinned()"
@unpinned="handleUnpinned()"
v-on:edit="handleEdit"
/>
@ -441,7 +443,15 @@
this.$nextTick(() => {
this.forceUpdateIdx++;
});
}
},
handlePinned() {
this.post.pinned = true;
},
handleUnpinned() {
this.post.pinned = false;
},
}
}
</script>

@ -228,7 +228,7 @@
Update
</a>
<span class="mx-1">·</span>
<a href="" class="text-danger font-weight-bold">
<a href="#" class="text-danger font-weight-bold" @click.prevent="handleDeleteAvatar()">
Delete
</a>
</p>
@ -256,7 +256,7 @@
Update
</a>
<span class="mx-1">·</span>
<a href="" class="text-danger font-weight-bold">
<a href="#" class="text-danger font-weight-bold" @click.prevent="handleDeleteHeader()">
Delete
</a>
</p>
@ -983,6 +983,30 @@
return `/groups/${this.groupId}/members?a=il&pid=${pid}`;
},
handleDeleteAvatar() {
if(!window.confirm('Are you sure you want to delete your group avatar image?')) {
return;
}
this.savingChanges = true;
axios.post('/api/v0/groups/' + this.group.id + '/settings/delete-avatar')
.then(res => {
this.savingChanges = false;
this.group = res.data;
});
},
handleDeleteHeader() {
if(!window.confirm('Are you sure you want to delete your group header image?')) {
return;
}
this.savingChanges = true;
axios.post('/api/v0/groups/' + this.group.id + '/settings/delete-header')
.then(res => {
this.savingChanges = false;
this.group = res.data;
});
},
undoBlock(type, val) {
let action = type == 'moderate' ? `unblock ${val}?` : `allow anyone to join without approval from ${val}?`;
swal({

@ -112,6 +112,21 @@
@click.prevent="unarchivePost(status)">
{{ $t('menu.unarchive') }}
</a>
<a
v-if="status && profile.id == status.account.id && !status.pinned"
class="list-group-item menu-option text-danger"
href="#"
@click.prevent="pinPost(status)">
{{ $t('menu.pin') }}
</a>
<a
v-if="status && profile.id == status.account.id && status.pinned"
class="list-group-item menu-option text-danger"
href="#"
@click.prevent="unpinPost(status)">
{{ $t('menu.unpin') }}
</a>
<a
v-if="config.ab.pue && status && profile.id == status.account.id && status.visibility !== 'archived'"
@ -976,6 +991,57 @@
}
})
},
pinPost(status) {
if(window.confirm(this.$t('menu.pinPostConfirm')) == false) {
return;
}
this.closeModals();
axios.post('/api/pixelfed/v1/statuses/' + status.id.toString() + '/pin')
.then(res => {
const data = res.data;
if(data.id && data.pinned) {
this.$emit('pinned');
swal('Pinned', 'Successfully pinned post to your profile', 'success');
} else {
swal('Error', 'An error occured when attempting to pin', 'error');
}
})
.catch(err => {
this.closeModals();
if(err.response?.data?.error) {
swal('Error', err.response?.data?.error, 'error');
}
});
},
unpinPost(status) {
if(window.confirm(this.$t('menu.unpinPostConfirm')) == false) {
return;
}
this.closeModals();
axios.post('/api/pixelfed/v1/statuses/' + status.id.toString() + '/unpin')
.then(res => {
const data = res.data;
if(data.id) {
this.$emit('unpinned');
swal('Unpinned', 'Successfully unpinned post from your profile', 'success');
} else {
swal('Error', data.error, 'error');
}
})
.catch(err => {
this.closeModals();
if(err.response?.data?.error) {
swal('Error', err.response?.data?.error, 'error');
} else {
window.location.reload()
}
});
},
}
}
</script>

File diff suppressed because it is too large Load Diff

@ -105,9 +105,9 @@
</p>
<p v-if="user.id != profile.id && (relationship.followed_by || relationship.muting || relationship.blocking)" class="mt-n3 text-center">
<span v-if="relationship.followed_by" class="badge badge-primary p-1">Follows you</span>
<span v-if="relationship.muting" class="badge badge-dark p-1 ml-1">Muted</span>
<span v-if="relationship.blocking" class="badge badge-danger p-1 ml-1">Blocked</span>
<span v-if="relationship.followed_by" class="badge badge-primary p-1">{{ $t("profile.followYou")}}</span>
<span v-if="relationship.muting" class="badge badge-dark p-1 ml-1">{{ $t("profile.muted")}}</span>
<span v-if="relationship.blocking" class="badge badge-danger p-1 ml-1">{{ $t("profile.blocked") }}</span>
</p>
</div>
@ -145,7 +145,7 @@
</router-link> -->
<a class="btn btn-light font-weight-bold btn-block follow-btn" href="/settings/home">{{ $t('profile.editProfile') }}</a>
<a v-if="!profile.locked" class="btn btn-light font-weight-bold btn-block follow-btn mt-md-n4" href="/i/web/my-portfolio">
My Portfolio
{{ $t("profile.myPortifolio") }}
<span class="badge badge-success ml-1">NEW</span>
</a>
</div>
@ -421,10 +421,10 @@
},
getJoinedDate() {
let d = new Date(this.profile.created_at);
let month = new Intl.DateTimeFormat("en-US", { month: "long" }).format(d);
let year = d.getFullYear();
return `${month} ${year}`;
return new Date(this.profile.created_at).toLocaleDateString(this.$i18n.locale, {
year: 'numeric',
month: 'long',
});
},
follow() {

@ -7,13 +7,13 @@
<div class="media-body font-weight-light">
<div v-if="n.type == 'favourite'">
<p class="my-0">
<a :href="displayProfileUrl(n.account)" @click.prevent="getProfileUrl(n.account)" class="font-weight-bold text-dark text-break" :title="n.account.acct">&commat;{{n.account.acct}}</a> {{ $t('notifications.liked') }} <a class="font-weight-bold" :href="displayPostUrl(n.status)" @click.prevent="getPostUrl(n.status)">post</a>.
<a :href="displayProfileUrl(n.account)" @click.prevent="getProfileUrl(n.account)" class="font-weight-bold text-dark text-break" :title="n.account.acct">&commat;{{n.account.acct}}</a> {{ $t('notifications.liked') }} <a class="font-weight-bold" :href="displayPostUrl(n.status)" @click.prevent="getPostUrl(n.status)">{{ $t("notifications.post")}}</a>.
</p>
</div>
<div v-else-if="n.type == 'comment'">
<p class="my-0">
<a :href="displayProfileUrl(n.account)" @click.prevent="getProfileUrl(n.account)" class="font-weight-bold text-dark text-break" :title="n.account.acct">&commat;{{n.account.acct}}</a> {{ $t('notifications.commented') }} <a class="font-weight-bold" :href="displayPostUrl(n.status)" @click.prevent="getPostUrl(n.status)">post</a>.
<a :href="displayProfileUrl(n.account)" @click.prevent="getProfileUrl(n.account)" class="font-weight-bold text-dark text-break" :title="n.account.acct">&commat;{{n.account.acct}}</a> {{ $t('notifications.commented') }} <a class="font-weight-bold" :href="displayPostUrl(n.status)" @click.prevent="getPostUrl(n.status)">{{ $t("notifications.post")}}</a>.
</p>
</div>
@ -25,7 +25,7 @@
<div v-else-if="n.type == 'story:react'">
<p class="my-0">
<a :href="displayProfileUrl(n.account)" @click.prevent="getProfileUrl(n.account)" class="font-weight-bold text-dark text-break" :title="n.account.acct">&commat;{{n.account.acct}}</a> {{ $t('notifications.reacted') }} <a class="font-weight-bold" v-bind:href="'/account/direct/t/'+n.account.id">story</a>.
<a :href="displayProfileUrl(n.account)" @click.prevent="getProfileUrl(n.account)" class="font-weight-bold text-dark text-break" :title="n.account.acct">&commat;{{n.account.acct}}</a> {{ $t('notifications.reacted') }} <a class="font-weight-bold" v-bind:href="'/account/direct/t/'+n.account.id">{{ $t('notifications.story') }}</a>.
</p>
</div>
@ -141,30 +141,36 @@
return text.slice(0, limit) + '...'
},
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";
timeAgo(ts) {
let date = new Date(ts);
let now = new Date();
let seconds = Math.floor((now - date) / 1000);
let interval = Math.floor(seconds / 31536000);
if (interval >= 1) {
return new Intl.RelativeTimeFormat(this.$i18n.locale, { numeric: 'auto' }).format(-interval, 'year');
}
interval = Math.floor(seconds / 2592000);
if (interval >= 1) {
return new Intl.RelativeTimeFormat(this.$i18n.locale, { numeric: 'auto' }).format(-interval, 'month');
}
interval = Math.floor(seconds / 604800);
if (interval >= 1) {
return new Intl.RelativeTimeFormat(this.$i18n.locale, { numeric: 'auto' }).format(-interval, 'week');
}
interval = Math.floor(seconds / 86400);
if (interval >= 1) {
return new Intl.RelativeTimeFormat(this.$i18n.locale, { numeric: 'auto' }).format(-interval, 'day');
}
interval = Math.floor(seconds / 3600);
if (interval >= 1) {
return new Intl.RelativeTimeFormat(this.$i18n.locale, { numeric: 'auto' }).format(-interval, 'hour');
}
interval = Math.floor(seconds / 60);
if (interval >= 1) {
return new Intl.RelativeTimeFormat(this.$i18n.locale, { numeric: 'auto' }).format(-interval, 'minute');
}
return new Intl.RelativeTimeFormat(this.$i18n.locale, { numeric: 'auto' }).format(-seconds, 'second');
},
mentionUrl(status) {

@ -3,7 +3,7 @@
<div class="card shadow-sm mb-3" style="overflow: hidden;border-radius: 15px !important;">
<div class="card-body pb-0">
<div class="d-flex justify-content-between align-items-center mb-3">
<span class="text-muted font-weight-bold">Notifications</span>
<span class="text-muted font-weight-bold">{{ $t("notifications.title")}}</span>
<div v-if="feed && feed.length">
<router-link to="/i/web/notifications" class="btn btn-outline-light btn-sm mr-2" style="color: #B8C2CC !important">
<i class="far fa-filter"></i>
@ -49,27 +49,28 @@
class="mr-2 rounded-circle shadow-sm"
:src="n.account.avatar"
width="32"
height="32"
onerror="this.onerror=null;this.src='/storage/avatars/default.png';">
<div class="media-body font-weight-light small">
<div v-if="n.type == 'favourite'">
<p class="my-0">
<a :href="getProfileUrl(n.account)" class="font-weight-bold text-dark word-break" :title="n.account.acct">{{n.account.local == false ? '@':''}}{{truncate(n.account.username)}}</a> liked your
<a :href="getProfileUrl(n.account)" class="font-weight-bold text-dark word-break" :title="n.account.acct">{{n.account.local == false ? '@':''}}{{truncate(n.account.username)}}</a> {{ $t("notifications.liked")}}
<span v-if="n.status && n.status.hasOwnProperty('media_attachments')">
<a class="font-weight-bold" v-bind:href="getPostUrl(n.status)" :id="'fvn-' + n.id" @click.prevent="goToPost(n.status)">post</a>.
<a class="font-weight-bold" v-bind:href="getPostUrl(n.status)" :id="'fvn-' + n.id" @click.prevent="goToPost(n.status)">{{ $t("notifications.post")}}</a>.
<b-popover :target="'fvn-' + n.id" title="" triggers="hover" placement="top" boundary="window">
<img :src="notificationPreview(n)" width="100px" height="100px" style="object-fit: cover;">
</b-popover>
</span>
<span v-else>
<a class="font-weight-bold" :href="getPostUrl(n.status)" @click.prevent="goToPost(n.status)">post</a>.
<a class="font-weight-bold" :href="getPostUrl(n.status)" @click.prevent="goToPost(n.status)">{{ $t("notifications.post")}}</a>.
</span>
</p>
</div>
<div v-else-if="n.type == 'autospam.warning'">
<p class="my-0">
Your recent <a :href="getPostUrl(n.status)" @click.prevent="goToPost(n.status)" class="font-weight-bold">post</a> has been unlisted.
{{ $t("notifications.youRecent")}} <a :href="getPostUrl(n.status)" @click.prevent="goToPost(n.status)" class="font-weight-bold">{{ $t("notifications.post")}}</a> {{ $t("notifications.hasUnlisted")}}.
</p>
<p class="mt-n1 mb-0">
<span class="small text-muted"><a href="#" class="font-weight-bold" @click.prevent="showAutospamInfo(n.status)">Click here</a> for more info.</span>
@ -77,64 +78,64 @@
</div>
<div v-else-if="n.type == 'comment'">
<p class="my-0">
<a :href="getProfileUrl(n.account)" class="font-weight-bold text-dark word-break" :title="n.account.acct">{{n.account.local == false ? '@':''}}{{truncate(n.account.username)}}</a> commented on your <a class="font-weight-bold" :href="getPostUrl(n.status)" @click.prevent="goToPost(n.status)">post</a>.
<a :href="getProfileUrl(n.account)" class="font-weight-bold text-dark word-break" :title="n.account.acct">{{n.account.local == false ? '@':''}}{{truncate(n.account.username)}}</a> {{ $t("notifications.commented")}} <a class="font-weight-bold" :href="getPostUrl(n.status)" @click.prevent="goToPost(n.status)">{{ $t("notifications.post")}}</a>.
</p>
</div>
<div v-else-if="n.type == 'group:comment'">
<p class="my-0">
<a :href="getProfileUrl(n.account)" class="font-weight-bold text-dark word-break" :title="n.account.acct">{{n.account.local == false ? '@':''}}{{truncate(n.account.username)}}</a> commented on your <a class="font-weight-bold" :href="n.group_post_url">group post</a>.
<a :href="getProfileUrl(n.account)" class="font-weight-bold text-dark word-break" :title="n.account.acct">{{n.account.local == false ? '@':''}}{{truncate(n.account.username)}}</a> {{ $t("notifications.commented")}} <a class="font-weight-bold" :href="n.group_post_url">{{ $t("notifications.groupPost") }}</a>.
</p>
</div>
<div v-else-if="n.type == 'story:react'">
<p class="my-0">
<a :href="getProfileUrl(n.account)" class="font-weight-bold text-dark word-break" :title="n.account.acct">{{n.account.local == false ? '@':''}}{{truncate(n.account.username)}}</a> reacted to your <a class="font-weight-bold" v-bind:href="'/i/web/direct/thread/'+n.account.id">story</a>.
<a :href="getProfileUrl(n.account)" class="font-weight-bold text-dark word-break" :title="n.account.acct">{{n.account.local == false ? '@':''}}{{truncate(n.account.username)}}</a> {{ $t("notifications.reacted")}} <a class="font-weight-bold" v-bind:href="'/i/web/direct/thread/'+n.account.id">{{ $t("notifications.story")}}</a>.
</p>
</div>
<div v-else-if="n.type == 'story:comment'">
<p class="my-0">
<a :href="getProfileUrl(n.account)" class="font-weight-bold text-dark word-break" :title="n.account.acct">{{n.account.local == false ? '@':''}}{{truncate(n.account.username)}}</a> commented on your <a class="font-weight-bold" v-bind:href="'/i/web/direct/thread/'+n.account.id">story</a>.
<a :href="getProfileUrl(n.account)" class="font-weight-bold text-dark word-break" :title="n.account.acct">{{n.account.local == false ? '@':''}}{{truncate(n.account.username)}}</a> {{ $t("notifications.commented")}} <a class="font-weight-bold" v-bind:href="'/i/web/direct/thread/'+n.account.id">{{ $t("notifications.story")}}</a>.
</p>
</div>
<div v-else-if="n.type == 'mention'">
<p class="my-0">
<a :href="getProfileUrl(n.account)" class="font-weight-bold text-dark word-break" :title="n.account.acct">{{n.account.local == false ? '@':''}}{{truncate(n.account.username)}}</a> <a class="font-weight-bold" v-bind:href="mentionUrl(n.status)" @click.prevent="goToPost(n.status)">mentioned</a> you.
<a :href="getProfileUrl(n.account)" class="font-weight-bold text-dark word-break" :title="n.account.acct">{{n.account.local == false ? '@':''}}{{truncate(n.account.username)}}</a> <a class="font-weight-bold" v-bind:href="mentionUrl(n.status)" @click.prevent="goToPost(n.status)">{{ $t("notifications.mentioned")}}</a> {{ $t("notifications.you")}}.
</p>
</div>
<div v-else-if="n.type == 'follow'">
<p class="my-0">
<a :href="getProfileUrl(n.account)" class="font-weight-bold text-dark word-break" :title="n.account.acct">{{n.account.local == false ? '@':''}}{{truncate(n.account.username)}}</a> followed you.
<a :href="getProfileUrl(n.account)" class="font-weight-bold text-dark word-break" :title="n.account.acct">{{n.account.local == false ? '@':''}}{{truncate(n.account.username)}}</a> {{ $t("notifications.followed")}} {{ $t("notifications.you")}}.
</p>
</div>
<div v-else-if="n.type == 'share'">
<p class="my-0">
<a :href="getProfileUrl(n.account)" class="font-weight-bold text-dark word-break" :title="n.account.acct">{{n.account.local == false ? '@':''}}{{truncate(n.account.username)}}</a> shared your <a class="font-weight-bold" :href="getPostUrl(n.status)" @click.prevent="goToPost(n.status)">post</a>.
<a :href="getProfileUrl(n.account)" class="font-weight-bold text-dark word-break" :title="n.account.acct">{{n.account.local == false ? '@':''}}{{truncate(n.account.username)}}</a> {{ $t("notifications.shared")}} <a class="font-weight-bold" :href="getPostUrl(n.status)" @click.prevent="goToPost(n.status)">{{ $t("notifications.post")}}</a>.
</p>
</div>
<div v-else-if="n.type == 'modlog'">
<p class="my-0">
<a :href="getProfileUrl(n.account)" class="font-weight-bold text-dark word-break" :title="n.account.acct">{{truncate(n.account.username)}}</a> updated a <a class="font-weight-bold" v-bind:href="n.modlog.url">modlog</a>.
<a :href="getProfileUrl(n.account)" class="font-weight-bold text-dark word-break" :title="n.account.acct">{{truncate(n.account.username)}}</a> {{ $t("notifications.updatedA")}} <a class="font-weight-bold" v-bind:href="n.modlog.url">modlog</a>.
</p>
</div>
<div v-else-if="n.type == 'tagged'">
<p class="my-0">
<a :href="getProfileUrl(n.account)" class="font-weight-bold text-dark word-break" :title="n.account.acct">{{n.account.local == false ? '@':''}}{{truncate(n.account.username)}}</a> tagged you in a <a class="font-weight-bold" v-bind:href="n.tagged.post_url">post</a>.
<a :href="getProfileUrl(n.account)" class="font-weight-bold text-dark word-break" :title="n.account.acct">{{n.account.local == false ? '@':''}}{{truncate(n.account.username)}}</a> {{ $t("notifications.tagged")}} <a class="font-weight-bold" v-bind:href="n.tagged.post_url">{{ $t("notifications.post")}}</a>.
</p>
</div>
<div v-else-if="n.type == 'direct'">
<p class="my-0">
<a :href="getProfileUrl(n.account)" class="font-weight-bold text-dark word-break" :title="n.account.acct">{{n.account.local == false ? '@':''}}{{truncate(n.account.username)}}</a> sent a <router-link class="font-weight-bold" :to="'/i/web/direct/thread/'+n.account.id">dm</router-link>.
<a :href="getProfileUrl(n.account)" class="font-weight-bold text-dark word-break" :title="n.account.acct">{{n.account.local == false ? '@':''}}{{truncate(n.account.username)}}</a> {{ $t("notifications.sentA")}} <router-link class="font-weight-bold" :to="'/i/web/direct/thread/'+n.account.id">dm</router-link>.
</p>
</div>
<div v-else-if="n.type == 'group.join.approved'">
<p class="my-0">
Your application to join the <a :href="n.group.url" class="font-weight-bold text-dark word-break" :title="n.group.name">{{truncate(n.group.name)}}</a> group was approved!
{{ $t("notifications.yourApplication")}} <a :href="n.group.url" class="font-weight-bold text-dark word-break" :title="n.group.name">{{truncate(n.group.name)}}</a> {{ $t("notifications.wasApproved")}}
</p>
</div>
<div v-else-if="n.type == 'group.join.rejected'">
<p class="my-0">
Your application to join <a :href="n.group.url" class="font-weight-bold text-dark word-break" :title="n.group.name">{{truncate(n.group.name)}}</a> was rejected.
{{ $t("notifications.yourApplication")}} <a :href="n.group.url" class="font-weight-bold text-dark word-break" :title="n.group.name">{{truncate(n.group.name)}}</a> {{ $t("notifications.wasRejected")}}
</p>
</div>
@ -146,11 +147,11 @@
<div v-else>
<p class="my-0">
We cannot display this notification at this time.
{{ $t("notifications.cannotDisplay")}}
</p>
</div>
</div>
<div class="small text-muted font-weight-bold" :title="n.created_at">{{timeAgo(n.created_at)}}</div>
<div class="small text-muted font-weight-bold" style="font-size: 0.575em;" st :title="n.created_at">{{timeAgo(n.created_at)}}</div>
</div>
</div>

@ -1,3 +1,5 @@
import VueI18n from 'vue-i18n';
require('./polyfill');
window._ = require('lodash');
window.Popper = require('popper.js').default;
@ -19,7 +21,7 @@ if (token) {
window.App = window.App || {};
window.App.redirect = function() {
document.querySelectorAll('a').forEach(function(i,k) {
document.querySelectorAll('a').forEach(function(i,k) {
let a = i.getAttribute('href');
if(a && a.length > 5 && a.startsWith('https://')) {
let url = new URL(a);
@ -31,7 +33,23 @@ window.App.redirect = function() {
}
window.App.boot = function() {
new Vue({ el: '#content'});
Vue.use(VueI18n);
let i18nMessages = {
en: require('./i18n/en.json'),
pt: require('./i18n/pt.json'),
};
let locale = document.querySelector('html').getAttribute('lang');
const i18n = new VueI18n({
locale: locale, // set locale
fallbackLocale: 'en',
messages: i18nMessages
});
new Vue({
el: '#content',
i18n,
});
}
window.addEventListener("load", () => {
@ -67,8 +85,8 @@ window.App.util = {
console.log('Unsupported method.');
}),
},
time: (function() {
return new Date;
time: (function() {
return new Date;
}),
version: 1,
format: {
@ -78,40 +96,36 @@ window.App.util = {
}
return new Intl.NumberFormat(locale, { notation: notation , compactDisplay: "short" }).format(count);
}),
timeAgo: function(ts) {
const date = new Date(ts);
const now = new Date();
const seconds = Math.floor((now - date) / 1000);
const secondsInYear = 60 * 60 * 24 * 365.25;
let interval = Math.floor(seconds / secondsInYear);
timeAgo: (function(ts) {
let date = new Date(ts);
let now = new Date();
let seconds = Math.floor((now - date) / 1000);
let interval = Math.floor(seconds / 31536000);
if (interval >= 1) {
return interval + "y";
return new Intl.RelativeTimeFormat('en', { numeric: 'auto', style: 'short' }).format(-interval, 'year');
}
interval = Math.floor(seconds / (60 * 60 * 24 * 7));
interval = Math.floor(seconds / 2592000);
if (interval >= 1) {
return interval + "w";
return new Intl.RelativeTimeFormat('en', { numeric: 'auto', style: 'short' }).format(-interval, 'month');
}
interval = Math.floor(seconds / (60 * 60 * 24));
interval = Math.floor(seconds / 604800);
if (interval >= 1) {
return interval + "d";
return new Intl.RelativeTimeFormat('en', { numeric: 'auto', style: 'short' }).format(-interval, 'week');
}
interval = Math.floor(seconds / (60 * 60));
interval = Math.floor(seconds / 86400);
if (interval >= 1) {
return interval + "h";
return new Intl.RelativeTimeFormat('en', { numeric: 'auto', style: 'short' }).format(-interval, 'day');
}
interval = Math.floor(seconds / 3600);
if (interval >= 1) {
return new Intl.RelativeTimeFormat('en', { numeric: 'auto', style: 'short' }).format(-interval, 'hour');
}
interval = Math.floor(seconds / 60);
if (interval >= 1) {
return interval + "m";
return new Intl.RelativeTimeFormat('en', { numeric: 'auto', style: 'short' }).format(-interval, 'minute');
}
return Math.floor(seconds) + "s";
},
return new Intl.RelativeTimeFormat('en', { numeric: 'auto', style: 'short' }).format(-seconds, 'second');
}),
timeAhead: (function(ts, short = true) {
let date = Date.parse(ts);
let diff = date - Date.parse(new Date());
@ -154,9 +168,9 @@ window.App.util = {
tag = '/i/redirect?url=' + encodeURIComponent(tag);
}
return tag;
return tag;
})
},
},
filters: [
['1984','filter-1977'],
['Azen','filter-aden'],

@ -16,7 +16,7 @@
</div>
<div v-if="relationship && relationship.blocking && warning" class="bg-white pt-3 border-bottom">
<div class="container">
<p class="text-center font-weight-bold">You are blocking this account</p>
<p class="text-center font-weight-bold">{{ $t("profile.blocking")}}</p>
<p class="text-center font-weight-bold">Click <a href="#" class="cursor-pointer" @click.prevent="warning = false;">here</a> to view profile</p>
</div>
</div>
@ -49,7 +49,7 @@
<div class="font-weight-light">
<span class="text-dark text-center">
<p class="font-weight-bold mb-0">{{formatCount(profile.statuses_count)}}</p>
<p class="text-muted mb-0 small">Posts</p>
<p class="text-muted mb-0 small">{{ $t("profile.posts")}}</p>
</span>
</div>
</li>
@ -57,7 +57,7 @@
<div v-if="profileSettings.followers.count" class="font-weight-light">
<a class="text-dark cursor-pointer text-center" v-on:click="followersModal()">
<p class="font-weight-bold mb-0">{{formatCount(profile.followers_count)}}</p>
<p class="text-muted mb-0 small">Followers</p>
<p class="text-muted mb-0 small">{{ $t("profile.followers")}}</p>
</a>
</div>
</li>
@ -65,7 +65,7 @@
<div v-if="profileSettings.following.count" class="font-weight-light">
<a class="text-dark cursor-pointer text-center" v-on:click="followingModal()">
<p class="font-weight-bold mb-0">{{formatCount(profile.following_count)}}</p>
<p class="text-muted mb-0 small">Following</p>
<p class="text-muted mb-0 small">{{ $t("profile.following")}}</p>
</a>
</div>
</li>
@ -86,7 +86,7 @@
<p v-if="sponsorList.patreon || sponsorList.liberapay || sponsorList.opencollective" class="text-center mt-3">
<button type="button" @click="showSponsorModal" class="btn btn-outline-secondary font-weight-bold py-0">
<i class="fas fa-heart text-danger"></i>
Donate
{{ $t("profile.sponsor")}}
</button>
</p>
</div>
@ -106,7 +106,7 @@
</span>
</span>
<span class="pl-4" v-if="owner && user.hasOwnProperty('id')">
<a class="btn btn-outline-secondary btn-sm" href="/settings/home" style="font-weight: 600;">Edit Profile</a>
<a class="btn btn-outline-secondary btn-sm" href="/settings/home" style="font-weight: 600;">{{ $t("profile.editProfile") }}</a>
</span>
<span class="pl-4">
<a class="fas fa-ellipsis-h fa-lg text-dark text-decoration-none" href="#" @click.prevent="visitorMenu"></a>
@ -117,19 +117,19 @@
<div class="font-weight-light pr-5">
<span class="text-dark">
<span class="font-weight-bold">{{formatCount(profile.statuses_count)}}</span>
Posts
{{ $t("profile.posts")}}
</span>
</div>
<div v-if="profileSettings.followers.count" class="font-weight-light pr-5">
<a class="text-dark cursor-pointer" v-on:click="followersModal()">
<span class="font-weight-bold">{{formatCount(profile.followers_count)}}</span>
Followers
{{ $t("profile.followers")}}
</a>
</div>
<div v-if="profileSettings.following.count" class="font-weight-light">
<a class="text-dark cursor-pointer" v-on:click="followingModal()">
<span class="font-weight-bold">{{formatCount(profile.following_count)}}</span>
Following
{{ $t("profile.following")}}
</a>
</div>
</div>
@ -141,11 +141,11 @@
<p v-if="profile.website"><a :href="profile.website" class="profile-website small" rel="me external nofollow noopener" target="_blank">{{formatWebsite(profile.website)}}</a></p>
<p class="d-flex small text-muted align-items-center">
<span v-if="profile.is_admin" class="btn btn-outline-danger btn-sm py-0 mr-3" title="Admin Account" data-toggle="tooltip">
Admin
{{ $t("profile.admin") }}
</span>
<span v-if="relationship && relationship.followed_by" class="btn btn-outline-muted btn-sm py-0 mr-3">Follows You</span>
<span v-if="relationship && relationship.followed_by" class="btn btn-outline-muted btn-sm py-0 mr-3">{{ $t("profile.followYou") }}</span>
<span>
Joined {{joinedAtFormat(profile.created_at)}}
{{$t("profile.joined")}} {{joinedAtFormat(profile.created_at)}}
</span>
</p>
</div>
@ -156,7 +156,7 @@
</div>
<div v-if="user && user.hasOwnProperty('id')" class="d-block d-md-none my-0 pt-3 border-bottom">
<p class="pt-3">
<button v-if="owner" class="btn btn-outline-secondary bg-white btn-sm py-1 btn-block text-center font-weight-bold text-dark border border-lighter" @click.prevent="redirect('/settings/home')">Edit Profile</button>
<button v-if="owner" class="btn btn-outline-secondary bg-white btn-sm py-1 btn-block text-center font-weight-bold text-dark border border-lighter" @click.prevent="redirect('/settings/home')">{{ $t("profile.editProfile")}}</button>
<button v-if="!owner && relationship.following" class="btn btn-outline-secondary bg-white btn-sm py-1 px-5 font-weight-bold text-dark border border-lighter" @click="followProfile">&nbsp;&nbsp; Unfollow &nbsp;&nbsp;</button>
<button v-if="!owner && !relationship.following" class="btn btn-primary btn-sm py-1 px-5 font-weight-bold" @click="followProfile">{{relationship.followed_by ? 'Follow Back' : '&nbsp;&nbsp;&nbsp;&nbsp; Follow &nbsp;&nbsp;&nbsp;&nbsp;'}}</button>
<!-- <button v-if="!owner" class="btn btn-outline-secondary bg-white btn-sm py-1 px-5 font-weight-bold text-dark border border-lighter mx-2">Message</button>
@ -1340,9 +1340,11 @@
return this.truncate(site, 60);
},
joinedAtFormat(created) {
let d = new Date(created);
return d.toDateString();
joinedAtFormat(created) {
return new Date(created).toLocaleDateString(this.$i18n.locale, {
year: 'numeric',
month: 'long',
});
},
archivesInfiniteLoader($state) {

@ -5,7 +5,7 @@
"comments": "Kommentare",
"like": "Gef\u00e4llt mir",
"liked": "Gef\u00e4llt",
"likes": "Gefiel",
"likes": "\"Gef\u00e4llt mir\"-Angaben",
"share": "Teilen",
"shared": "Geteilt",
"shares": "Geteilt",

@ -62,6 +62,7 @@
"requests": "Requests"
},
"notifications": {
"title": "Notifications",
"liked": "liked your",
"commented": "commented on your",
"reacted": "reacted to your",
@ -80,7 +81,24 @@
"modlog": "modlog",
"post": "post",
"story": "story",
"noneFound": "No notifications found"
"noneFound": "No notifications found",
"youRecent": "You recent",
"hasUnlisted": "has been unlisted",
"cannotDisplay": "We cannot display this notification at this time.",
"followRequest": "Follow Requests",
"filteringResults": "Filtering results may not include older notifications",
"mentions": "Mentions",
"mentionsDescription": "Replies to your posts and posts you were mentioned in",
"likes": "Likes",
"likesDescription": "Accounts that liked your posts",
"followers": "Followers",
"followersDescription": "Accounts that followed you",
"reblogs": "Reblogs",
"reblogsDescription": "Accounts that shared or reblogged your posts",
"dms": "DMs",
"dmsDescription": "Direct messages you have with other accounts",
"accept": "Accept",
"reject": "Reject"
},
"post": {
"shareToFollowers": "Share to followers",
@ -100,7 +118,24 @@
"followRequested": "Follow Requested",
"joined": "Joined",
"emptyCollections": "We can't seem to find any collections",
"emptyPosts": "We can't seem to find any posts"
"emptyPosts": "We can't seem to find any posts",
"blocking": "You are blocking this account",
"sponsor": "Donate",
"followYou": "Follows You",
"archives": "Archives",
"bookmarks": "Bookmarks",
"likes": "Likes",
"muted": "Muted",
"blocked": "Blocked",
"myPortifolio": "My Portfolio",
"private": "This profile is private",
"public": "Public",
"draft": "Draft",
"emptyLikes": "We can't seem to find any posts you have liked",
"emptyBookmarks": "We can't seem to find any posts you have bookmarked",
"emptyArchives": "We can't seem to find any archived posts",
"untitled": "Untitled",
"noDescription": "No description available"
},
"menu": {
"viewPost": "View Post",
@ -143,7 +178,11 @@
"embedConfirmText": "By using this embed, you agree to our",
"deletePostConfirm": "Are you sure you want to delete this post?",
"archivePostConfirm": "Are you sure you want to archive this post?",
"unarchivePostConfirm": "Are you sure you want to unarchive this post?"
"unarchivePostConfirm": "Are you sure you want to unarchive this post?",
"pin": "Pin",
"unpin": "Unpin",
"pinPostConfirm": "Are you sure you want to pin this post?",
"unpinPostConfirm": "Are you sure you want to unpin this post?"
},
"story": {
"add": "Add Story"

@ -72,8 +72,8 @@
"mentioned": "mainittu",
"you": "sin\u00e4",
"yourApplication": "Liittymist\u00e4 koskeva hakemuksesi",
"applicationApproved": "oli hyv\u00e4ksytty!",
"applicationRejected": "oli hyl\u00e4tty. Voit hakea uudelleen 6 kuukauden kuluttua.",
"applicationApproved": "hyv\u00e4ksyttiin!",
"applicationRejected": "hyl\u00e4ttiin. Voit hakea uudelleen 6 kuukauden kuluttua.",
"dm": "yv",
"groupPost": "ryhm\u00e4viesti",
"modlog": "modelogi",
@ -114,7 +114,7 @@
"addCW": "Lis\u00e4\u00e4 sis\u00e4lt\u00f6varoitus",
"removeCW": "Poista sis\u00e4lt\u00f6varoitus",
"markAsSpammer": "Merkitse roskapostittajaksi",
"markAsSpammerText": "Unlist + SV olevat ja tulevat julkaisut",
"markAsSpammerText": "Poista listalta + SV olevat ja tulevat julkaisut",
"spam": "Roskaposti",
"sensitive": "Arkaluonteista sis\u00e4lt\u00f6\u00e4",
"abusive": "Hy\u00f6kk\u00e4\u00e4v\u00e4 tai haitallinen",
@ -132,8 +132,8 @@
"modRemoveCWConfirm": "Haluatko varmasti poistaa julkaisun sis\u00e4lt\u00f6varoituksen?",
"modRemoveCWSuccess": "Sis\u00e4lt\u00f6varoitus poistettu",
"modUnlistConfirm": "Haluatko varmasti piilottaa julkaisun?",
"modUnlistSuccess": "Julkaisu piilotettu onnistuneesti",
"modMarkAsSpammerConfirm": "Haluatko varmasti merkit\u00e4 k\u00e4ytt\u00e4j\u00e4n roskapostittajaksi? Kaikki nykyiset ja tulevat julkaisut saavat sis\u00e4lt\u00f6varoituksen ja julkaisut piilotetaan julkisilta aikajanoilta.",
"modUnlistSuccess": "Julkaisun piilotus onnistui",
"modMarkAsSpammerConfirm": "Haluatko varmasti merkit\u00e4 k\u00e4ytt\u00e4j\u00e4n roskapostittajaksi? Kaikki nykyiset ja tulevat julkaisut saavat sis\u00e4lt\u00f6varoituksen ja julkaisut piilotetaan aikajanoilta.",
"modMarkAsSpammerSuccess": "Tili merkitty roskapostittajaksi",
"toFollowers": "seuraajille",
"showCaption": "N\u00e4yt\u00e4 kuvaus",
@ -151,7 +151,7 @@
"peopleYouMayKnow": "Ihmisi\u00e4, jotka saatat tuntea",
"onboarding": {
"welcome": "Tervetuloa",
"thisIsYourHomeFeed": "T\u00e4m\u00e4 on kotisy\u00f6tteesi. Aikaj\u00e4rjestyksess\u00e4 oleva sy\u00f6te seuraamiesi k\u00e4ytt\u00e4jien julkaisuista.",
"thisIsYourHomeFeed": "T\u00e4m\u00e4 on kotisy\u00f6tteesi, aikaj\u00e4rjestyksess\u00e4 oleva sy\u00f6te seuraamiesi k\u00e4ytt\u00e4jien julkaisuista.",
"letUsHelpYouFind": "Anna meid\u00e4n auttaa l\u00f6yt\u00e4m\u00e4\u00e4n mielenkiintoisia ihmisi\u00e4 seurattavaksi",
"refreshFeed": "P\u00e4ivit\u00e4 sy\u00f6tteeni"
}
@ -164,7 +164,7 @@
"selectReason": "Valitse syy",
"reported": "Ilmoitettu",
"sendingReport": "L\u00e4hetet\u00e4\u00e4n ilmoitusta",
"thanksMsg": "Kiitos ilmoituksesta. Sin\u00e4 ja kaltaisesi autatte pit\u00e4m\u00e4\u00e4n yhteis\u00f6n turvallisena!",
"contactAdminMsg": "Jos haluaisit ottaa yhteytt\u00e4 yll\u00e4pitoom t\u00e4st\u00e4 julkaisusta tai ilmoittaa sen"
"thanksMsg": "Kiitos ilmoituksesta, sin\u00e4 ja kaltaisesi autatte pit\u00e4m\u00e4\u00e4n yhteis\u00f6n turvallisena!",
"contactAdminMsg": "Jos haluaisit ottaa yhteytt\u00e4 yll\u00e4pitoon t\u00e4h\u00e4n julkaisuun liittyen tai ilmoittaa sen"
}
}

@ -1,7 +1,7 @@
{
"common": {
"comment": "Komentari",
"commented": "Dikomentari",
"comment": "Komentar",
"commented": "Mengomentari",
"comments": "Komentar",
"like": "Sukai",
"liked": "Disukai",

@ -6,8 +6,8 @@
"like": "Gosto",
"liked": "Gostei",
"likes": "Gostos",
"share": "Partilhar",
"shared": "Partilhado",
"share": "Compartilhar",
"shared": "Compartilhado",
"shares": "Partilhas",
"unshare": "Despartilhar",
"bookmark": "Favorito",
@ -15,71 +15,90 @@
"copyLink": "Copiar link",
"delete": "Eliminar",
"error": "Erro",
"errorMsg": "Algo correu mal. Tenta novamente mais tarde.",
"oops": "Oops!",
"errorMsg": "Algo correu mal. Por favor, tente novamente mais tarde.",
"oops": "Opa!",
"other": "Outro",
"readMore": "Ler mais",
"success": "Sucesso",
"proceed": "Continuar",
"next": "Seguinte",
"close": "Fechar",
"clickHere": "clica aqui",
"clickHere": "clique aqui",
"sensitive": "Sens\u00edvel",
"sensitiveContent": "Conte\u00fado sens\u00edvel",
"sensitiveContentWarning": "Esta publica\u00e7\u00e3o pode conter conte\u00fado sens\u00edvel"
"sensitiveContentWarning": "Este post pode conter conte\u00fado sens\u00edvel"
},
"site": {
"terms": "Termos de Utiliza\u00e7\u00e3o",
"terms": "Termos de Uso",
"privacy": "Pol\u00edtica de Privacidade"
},
"navmenu": {
"search": "Pesquisar",
"admin": "Painel de administra\u00e7\u00e3o",
"homeFeed": "In\u00edcio",
"search": "Pesquisa",
"admin": "Painel de Administra\u00e7\u00e3o",
"homeFeed": "Inicio",
"localFeed": "Feed local",
"globalFeed": "Feed global",
"discover": "Descobrir",
"directMessages": "Mensagens diretas",
"directMessages": "Mensagens Diretas",
"notifications": "Notifica\u00e7\u00f5es",
"groups": "Grupos",
"stories": "Stories",
"profile": "Perfil",
"drive": "Disco",
"drive": "Drive",
"settings": "Defini\u00e7\u00f5es",
"appearance": "Apar\u00eancia",
"compose": "Criar novo",
"logout": "Terminar sess\u00e3o",
"logout": "Terminar Sess\u00e3o",
"about": "Sobre",
"help": "Ajuda",
"language": "Idioma",
"privacy": "Privacidade",
"terms": "Termos",
"backToPreviousDesign": "Voltar ao design antigo"
"backToPreviousDesign": "Voltar ao design anterior"
},
"directMessages": {
"inbox": "Caixa de entrada",
"inbox": "Caixa de Entrada",
"sent": "Enviadas",
"requests": "Pedidos"
},
"notifications": {
"liked": "gostou do seu",
"commented": "comentou no seu",
"reacted": "reagiu ao seu",
"shared": "partilhou o teu",
"tagged": "etiquetou-te numa publica\u00e7\u00e3o",
"updatedA": "atualizou uma",
"sentA": "enviou uma",
"followed": "seguiu-te",
"mentioned": "mencionou-te",
"you": "tu",
"yourApplication": "O teu pedido de ades\u00e3o",
"applicationApproved": "foi aprovado!",
"applicationRejected": "foi rejeitado. Podes voltar a candidatar-te dentro de 6 meses.",
"dm": "md",
"title": "Notifica\u00e7\u00f5es",
"liked": "curtiu sua",
"commented": "comentou na sua",
"reacted": "reagiu \u00e0 sua",
"shared": "compartilhou a sua",
"tagged": "marcou voc\u00ea numa publica\u00e7\u00e3o",
"updatedA": "atualizou",
"sentA": "enviou um",
"followed": "seguiu",
"mentioned": "mencionou",
"you": "voc\u00ea",
"yourApplication": "A sua candidatura para se juntar",
"applicationApproved": "foi aprovada!",
"applicationRejected": "foi rejeitada. Voc\u00ea pode inscrever-se novamente em 6 meses.",
"dm": "mensagem direta",
"groupPost": "publica\u00e7\u00e3o de grupo",
"modlog": "hist\u00f3rico de modera\u00e7\u00e3o",
"post": "publica\u00e7\u00e3o",
"story": "est\u00f3ria",
"noneFound": "Nenhuma notifica\u00e7\u00e3o encontrada"
"noneFound": "Nenhuma notifica\u00e7\u00e3o encontrada",
"youRecent": "Voc\u00ea recente",
"hasUnlisted": "foi removida da lista",
"cannotDisplay": "N\u00e3o podemos exibir esta notifica\u00e7\u00e3o no momento.",
"followRequest": "Pedidos de Seguimento",
"filteringResults": "Os resultados do filtro podem n\u00e3o incluir notifica\u00e7\u00f5es mais antigas",
"mentions": "Men\u00e7\u00f5es",
"mentionsDescription": "Respostas \u00e0s suas publica\u00e7\u00f5es e publica\u00e7\u00f5es em que voc\u00ea foi mencionado",
"likes": "Curtidas",
"likesDescription": "Contas que curtiram das suas publica\u00e7\u00f5es",
"followers": "Seguidores",
"followersDescription": "Contas que seguiram voc\u00ea",
"reblogs": "Reblogs",
"reblogsDescription": "Contas que compartilharam ou reblogaram suas publica\u00e7\u00f5es",
"dms": "DMs",
"dmsDescription": "Mensagens diretas que voc\u00ea tem com outras contas",
"accept": "Aceitar",
"reject": "Rejeitar"
},
"post": {
"shareToFollowers": "Partilhar com os seguidores",
@ -90,16 +109,33 @@
"profile": {
"posts": "Publica\u00e7\u00f5es",
"followers": "Seguidores",
"following": "Seguindo",
"following": "A seguir",
"admin": "Administrador",
"collections": "Cole\u00e7\u00f5es",
"follow": "Seguir",
"unfollow": "Deixar de seguir",
"editProfile": "Editar perfil",
"editProfile": "Editar Perfil",
"followRequested": "Pedido para seguir enviado",
"joined": "Juntou-se",
"emptyCollections": "N\u00e3o conseguimos encontrar nenhuma cole\u00e7\u00e3o",
"emptyPosts": "N\u00e3o conseguimos encontrar nenhuma publica\u00e7\u00e3o"
"emptyPosts": "N\u00e3o conseguimos encontrar nenhuma publica\u00e7\u00e3o",
"blocking": "Voc\u00ea est\u00e1 bloqueando esta conta",
"sponsor": "Doar",
"followYou": "Segue voc\u00ea",
"archives": "Arquivados",
"bookmarks": "Favoritos",
"likes": "Curtidas",
"muted": "Silenciado",
"blocked": "Bloqueado",
"myPortifolio": "Meu Portf\u00f3lio",
"private": "Este perfil \u00e9 privado",
"public": "P\u00fablico",
"draft": "Rascunho",
"emptyLikes": "N\u00e3o conseguimos encontrar nenhuma publica\u00e7\u00e3o que voc\u00ea tenha curtido",
"emptyBookmarks": "N\u00e3o conseguimos encontrar nenhuma publica\u00e7\u00e3o nos seus favoritos",
"emptyArchives": "N\u00e3o conseguimos encontrar nenhuma publica\u00e7\u00e3o arquivada",
"untitled": "Sem t\u00edtulo",
"noDescription": "Nenhuma descri\u00e7\u00e3o dispon\u00edvel"
},
"menu": {
"viewPost": "Ver publica\u00e7\u00e3o",
@ -109,62 +145,79 @@
"archive": "Arquivar",
"unarchive": "Retirar do arquivo",
"embed": "Incorporar",
"selectOneOption": "Seleciona uma das seguintes op\u00e7\u00f5es",
"selectOneOption": "Selecione uma das seguintes op\u00e7\u00f5es",
"unlistFromTimelines": "Remover das cronologias",
"addCW": "Adicionar aviso de conte\u00fado",
"removeCW": "Remover aviso de conte\u00fado",
"markAsSpammer": "Marcar como spammer",
"markAsSpammer": "Marcar como Spammer",
"markAsSpammerText": "Remover das cronologias e adicionar um aviso de conte\u00fado \u00e0s publica\u00e7\u00f5es existentes e futuras",
"spam": "Spam",
"sensitive": "Conte\u00fado sens\u00edvel",
"spam": "Lixo Eletr\u00f4nico",
"sensitive": "Conte\u00fado Sens\u00edvel",
"abusive": "Abusivo ou prejudicial",
"underageAccount": "Conta de menor de idade",
"copyrightInfringement": "Viola\u00e7\u00e3o de direitos de autor",
"impersonation": "Roubo de identidade",
"scamOrFraud": "Esquema ou fraude",
"confirmReport": "Confirmar den\u00fancia",
"confirmReportText": "Tens a certeza que desejas denunciar esta mensagem?",
"confirmReportText": "Tem a certeza que deseja denunciar esta mensagem?",
"reportSent": "Den\u00fancia enviada!",
"reportSentText": "Recebemos com sucesso a tua den\u00fancia.",
"reportSentText": "Recebemos com sucesso a sua den\u00fancia.",
"reportSentError": "Ocorreu um erro ao denunciar este conte\u00fado.",
"modAddCWConfirm": "Tens a certeza que pretendes adicionar um aviso de conte\u00fado \u00e0 publica\u00e7\u00e3o?",
"modCWSuccess": "Adicionaste com sucesso um aviso de conte\u00fado",
"modRemoveCWConfirm": "Tens a certeza que pretendes remover o aviso de conte\u00fado desta publica\u00e7\u00e3o?",
"modRemoveCWSuccess": "Removeste com sucesso o aviso de conte\u00fado",
"modAddCWConfirm": "Tem a certeza que pretende adicionar um aviso de conte\u00fado \u00e0 publica\u00e7\u00e3o?",
"modCWSuccess": "Adicionou com sucesso um aviso de conte\u00fado",
"modRemoveCWConfirm": "Tem a certeza que pretende remover o aviso de conte\u00fado desta publica\u00e7\u00e3o?",
"modRemoveCWSuccess": "Removeu com sucesso o aviso de conte\u00fado",
"modUnlistConfirm": "Tem a certeza que pretende deslistar este post?",
"modUnlistSuccess": "Deslistou com sucesso este post",
"modMarkAsSpammerConfirm": "Tem a certeza que deseja marcar este utilizador como spammer? Todos os posts existentes e futuros ser\u00e3o deslistados da timeline e o alerta de conte\u00fado ser\u00e1 aplicado.",
"modMarkAsSpammerConfirm": "Voc\u00ea realmente quer denunciar este usu\u00e1rio por spam? Todas as suas publica\u00e7\u00f5es anteriores e futuras ser\u00e3o marcadas com um aviso de conte\u00fado e removidas das linhas do tempo.",
"modMarkAsSpammerSuccess": "Marcou com sucesso esta conta como spammer",
"toFollowers": "para Seguidores",
"showCaption": "Mostar legenda",
"showLikes": "Mostrar gostos",
"toFollowers": "para seguidores",
"showCaption": "Exibir legendas",
"showLikes": "Mostrar Gostos",
"compactMode": "Modo compacto",
"embedConfirmText": "Ao utilizar este conte\u00fado, concordas com:",
"deletePostConfirm": "Tens a certeza que pretendes eliminar esta publica\u00e7\u00e3o?",
"archivePostConfirm": "Tens a certeza que pretendes arquivar esta publica\u00e7\u00e3o?",
"unarchivePostConfirm": "Tem a certeza que pretende desarquivar este post?"
"embedConfirmText": "Ao usar de forma \u201cembed\u201d, voc\u00ea concorda com nossas",
"deletePostConfirm": "Tem a certeza que pretende apagar esta publica\u00e7\u00e3o?",
"archivePostConfirm": "Tem a certeza que pretende arquivar esta publica\u00e7\u00e3o?",
"unarchivePostConfirm": "Tem a certeza que pretende desarquivar este post?",
"pin": "Fixar",
"unpin": "Desfixar",
"pinPostConfirm": "Tem certeza de que deseja fixar esta publica\u00e7\u00e3o?",
"unpinPostConfirm": "Tem certeza de que deseja desafixar esta publica\u00e7\u00e3o?"
},
"story": {
"add": "Adicionar Storie"
"add": "Adicionar Story"
},
"timeline": {
"peopleYouMayKnow": "Pessoas que talvez conhe\u00e7as",
"peopleYouMayKnow": "Pessoas que talvez conhe\u00e7a",
"onboarding": {
"welcome": "Bem-vindo",
"thisIsYourHomeFeed": "Este \u00e9 a tua cronologia inicial pessoal, com publica\u00e7\u00f5es em ordem cronol\u00f3gica das contas que segue.",
"letUsHelpYouFind": "Deixa-nos ajudar-te a encontrar algumas pessoas interessantes para seguires",
"refreshFeed": "Atualizar a minha cronologia"
"thisIsYourHomeFeed": "Este \u00e9 o seu feed pessoal, com publica\u00e7\u00f5es em ordem cronol\u00f3gica das contas que segue.",
"letUsHelpYouFind": "Deixe-nos ajudar a encontrar algumas pessoas interessantes para seguir",
"refreshFeed": "Atualizar o meu feed"
}
},
"hashtags": {
"emptyFeed": "N\u00e3o conseguimos encontrar publica\u00e7\u00f5es com essa hashtag"
"emptyFeed": "N\u00e3o encontramos nenhuma publica\u00e7\u00e3o com esta hashtag"
},
"report": {
"report": "Denunciar",
"selectReason": "Seleciona um motivo",
"selectReason": "Selecione uma raz\u00e3o",
"reported": "Denunciado",
"sendingReport": "A enviar den\u00fancia",
"thanksMsg": "Obrigado pela den\u00fancia, as pessoas como tu ajudam a manter a nossa comunidade segura!",
"contactAdminMsg": "Se quiseres entrar em contacto com um administrador sobre esta publica\u00e7\u00e3o ou den\u00fancia"
"thanksMsg": "Obrigado pela den\u00fancia, pessoas como voc\u00ea ajudam a manter a nossa comunidade segura!",
"contactAdminMsg": "Se quiser entrar em contato com um administrador acerca desta publica\u00e7\u00e3o ou den\u00fancia"
},
"appearance": {
"theme": "Tema",
"profileLayout": "Layout do Perfil",
"compactPreviews": "Pr\u00e9-visualiza\u00e7\u00f5es Compactas",
"loadComments": "Carregar Coment\u00e1rios",
"hideStats": "Ocultar Contagens e Estat\u00edsticas",
"auto": "Autom\u00e1tico",
"lightMode": "Modo Claro",
"darkMode": "Modo Escuro",
"grid": "Grade",
"masonry": "Mansory",
"feed": "Feed"
}
}

@ -697,40 +697,36 @@ window.App.util = {
}
return new Intl.NumberFormat(locale, { notation: notation , compactDisplay: "short" }).format(count);
}),
timeAgo: function(ts) {
const date = new Date(ts);
const now = new Date();
const seconds = Math.floor((now - date) / 1000);
const secondsInYear = 60 * 60 * 24 * 365.25;
let interval = Math.floor(seconds / secondsInYear);
if (interval >= 1) {
return interval + "y";
}
interval = Math.floor(seconds / (60 * 60 * 24 * 7));
if (interval >= 1) {
return interval + "w";
}
interval = Math.floor(seconds / (60 * 60 * 24));
if (interval >= 1) {
return interval + "d";
}
interval = Math.floor(seconds / (60 * 60));
if (interval >= 1) {
return interval + "h";
}
interval = Math.floor(seconds / 60);
if (interval >= 1) {
return interval + "m";
}
return Math.floor(seconds) + "s";
},
timeAgo: (function(ts) {
let date = new Date(ts);
let now = new Date();
let seconds = Math.floor((now - date) / 1000);
let interval = Math.floor(seconds / 31536000);
if (interval >= 1) {
return new Intl.RelativeTimeFormat(i18n.locale, { numeric: 'auto', style: 'short' }).format(-interval, 'year');
}
interval = Math.floor(seconds / 2592000);
if (interval >= 1) {
return new Intl.RelativeTimeFormat(i18n.locale, { numeric: 'auto', style: 'short' }).format(-interval, 'month');
}
interval = Math.floor(seconds / 604800);
if (interval >= 1) {
return new Intl.RelativeTimeFormat(i18n.locale, { numeric: 'auto', style: 'short' }).format(-interval, 'week');
}
interval = Math.floor(seconds / 86400);
if (interval >= 1) {
return new Intl.RelativeTimeFormat(i18n.locale, { numeric: 'auto', style: 'short' }).format(-interval, 'day');
}
interval = Math.floor(seconds / 3600);
if (interval >= 1) {
return new Intl.RelativeTimeFormat(i18n.locale, { numeric: 'auto', style: 'short' }).format(-interval, 'hour');
}
interval = Math.floor(seconds / 60);
if (interval >= 1) {
return new Intl.RelativeTimeFormat(i18n.locale, { numeric: 'auto', style: 'short' }).format(-interval, 'minute');
}
return new Intl.RelativeTimeFormat(i18n.locale, { numeric: 'auto', style: 'short' }).format(-seconds, 'second');
}),
timeAhead: (function(ts, short = true) {
let date = Date.parse(ts);
let diff = date - Date.parse(new Date());

@ -81,6 +81,7 @@ return [
],
'notifications' => [
'title' => 'Notifications',
'liked' => 'liked your',
'commented' => 'commented on your',
'reacted' => 'reacted to your',
@ -104,6 +105,23 @@ return [
'post' => 'post',
'story' => 'story',
'noneFound' => 'No notifications found',
'youRecent' => 'You recent',
'hasUnlisted' => 'has been unlisted',
'cannotDisplay' => 'We cannot display this notification at this time.',
'followRequest' => 'Follow Requests',
'filteringResults' => 'Filtering results may not include older notifications',
'mentions' => 'Mentions',
'mentionsDescription' => 'Replies to your posts and posts you were mentioned in',
'likes' => 'Likes',
'likesDescription' => 'Accounts that liked your posts',
'followers' => 'Followers',
'followersDescription' => 'Accounts that followed you',
'reblogs' => 'Reblogs',
'reblogsDescription' => 'Accounts that shared or reblogged your posts',
'dms' => 'DMs',
'dmsDescription' => 'Direct messages you have with other accounts',
'accept' => 'Accept',
'reject' => 'Reject'
],
'post' => [
@ -127,12 +145,30 @@ return [
'emptyCollections' => 'We can\'t seem to find any collections',
'emptyPosts' => 'We can\'t seem to find any posts',
'blocking' => 'You are blocking this account',
'sponsor' => 'Donate',
'followYou' => 'Follows You',
'archives' => 'Archives',
'bookmarks' => 'Bookmarks',
'likes' => 'Likes',
'muted' => 'Muted',
'blocked' => 'Blocked',
'myPortifolio' => 'My Portfolio',
'private' => 'This profile is private',
'public' => 'Public',
'draft' => 'Draft',
'emptyLikes' => 'We can\'t seem to find any posts you have liked',
'emptyBookmarks' => 'We can\'t seem to find any posts you have bookmarked',
'emptyArchives' => 'We can\'t seem to find any archived posts',
'untitled' => 'Untitled',
'noDescription' => 'No description available'
],
'menu' => [
'viewPost' => 'View Post',
'viewProfile' => 'View Profile',
'moderationTools' => 'Moderation Tools',
'menu' => [
'viewPost' => 'View Post',
'viewProfile' => 'View Profile',
'moderationTools' => 'Moderation Tools',
'report' => 'Report',
'archive' => 'Archive',
'unarchive' => 'Unarchive',
@ -176,6 +212,10 @@ return [
'deletePostConfirm' => 'Are you sure you want to delete this post?',
'archivePostConfirm' => 'Are you sure you want to archive this post?',
'unarchivePostConfirm' => 'Are you sure you want to unarchive this post?',
'pin' => "Pin",
'unpin' => "Unpin",
'pinPostConfirm' => 'Are you sure you want to pin this post?',
'unpinPostConfirm' => 'Are you sure you want to unpin this post?'
],
'story' => [

@ -80,30 +80,44 @@ return [
'requests' => 'Pedidos'
],
'notifications' => [
'liked' => 'curtiu seu',
'commented' => 'comentou em seu',
'reacted' => 'reagiu ao seu',
'shared' => 'compartilhou seu',
'tagged' => 'marcou você em um',
'updatedA' => 'atualizou um(a)',
'sentA' => 'enviou um',
'followed' => 'seguiu',
'mentioned' => 'mencionou',
'you' => 'você',
'yourApplication' => 'A sua candidatura para se juntar',
'applicationApproved' => 'foi aprovado!',
'applicationRejected' => 'foi rejeitado. Você pode se inscrever novamente para participar em 6 meses.',
'dm' => 'mensagem direta',
'groupPost' => 'postagem do grupo',
'modlog' => 'histórico de moderação',
'post' => 'publicação',
'story' => 'história',
'noneFound' => 'Nenhuma notificação encontrada',
'notifications' => [
'title' => 'Notificações',
'liked' => 'curtiu sua',
'commented' => 'comentou na sua',
'reacted' => 'reagiu à sua',
'shared' => 'compartilhou a sua',
'tagged' => 'marcou você numa publicação',
'updatedA' => 'atualizou',
'sentA' => 'enviou um',
'followed' => 'seguiu',
'mentioned' => 'mencionou',
'you' => 'você',
'yourApplication' => 'A sua candidatura para se juntar',
'applicationApproved' => 'foi aprovada!',
'applicationRejected' => 'foi rejeitada. Você pode inscrever-se novamente em 6 meses.',
'dm' => 'mensagem direta',
'groupPost' => 'publicação de grupo',
'modlog' => 'histórico de moderação',
'post' => 'publicação',
'story' => 'estória',
'noneFound' => 'Nenhuma notificação encontrada',
'youRecent' => 'Você recente',
'hasUnlisted' => 'foi removida da lista',
'cannotDisplay' => 'Não podemos exibir esta notificação no momento.',
'followRequest' => 'Pedidos de Seguimento',
'filteringResults' => 'Os resultados do filtro podem não incluir notificações mais antigas',
'mentions' => 'Menções',
'mentionsDescription' => 'Respostas às suas publicações e publicações em que você foi mencionado',
'likes' => 'Curtidas',
'likesDescription' => 'Contas que curtiram das suas publicações',
'followers' => 'Seguidores',
'followersDescription' => 'Contas que seguiram você',
'reblogs' => 'Reblogs',
'reblogsDescription' => 'Contas que compartilharam ou reblogaram suas publicações',
'dms' => 'DMs',
'dmsDescription' => 'Mensagens diretas que você tem com outras contas',
'accept' => 'Aceitar',
'reject' => 'Rejeitar'
],
'post' => [
@ -127,6 +141,24 @@ return [
'emptyCollections' => 'Não conseguimos encontrar nenhuma coleção',
'emptyPosts' => 'Não conseguimos encontrar nenhuma publicação',
'blocking' => 'Você está bloqueando esta conta',
'sponsor' => 'Doar',
'followYou' => 'Segue você',
'archives' => 'Arquivados',
'bookmarks' => 'Favoritos',
'likes' => 'Curtidas',
'muted' => 'Silenciado',
'blocked' => 'Bloqueado',
'myPortifolio' => 'Meu Portfólio',
'private' => 'Este perfil é privado',
'public' => 'Público',
'draft' => 'Rascunho',
'emptyLikes' => 'Não conseguimos encontrar nenhuma publicação que você tenha curtido',
'emptyBookmarks' => 'Não conseguimos encontrar nenhuma publicação nos seus favoritos',
'emptyArchives' => 'Não conseguimos encontrar nenhuma publicação arquivada',
'untitled' => 'Sem título',
'noDescription' => 'Nenhuma descrição disponível'
],
'menu' => [
@ -176,6 +208,10 @@ return [
'deletePostConfirm' => 'Tem a certeza que pretende apagar esta publicação?',
'archivePostConfirm' => 'Tem a certeza que pretende arquivar esta publicação?',
'unarchivePostConfirm' => 'Tem a certeza que pretende desarquivar este post?',
'pin' => "Fixar",
'unpin' => "Desfixar",
"pinPostConfirm" => "Tem certeza de que deseja fixar esta publicação?",
"unpinPostConfirm" => "Tem certeza de que deseja desafixar esta publicação?"
],
'story' => [

@ -21,7 +21,7 @@
<div class="form-check pb-3">
<input class="form-check-input" type="checkbox" name="is_private" id="is_private" {{$settings->is_private ? 'checked=""':''}}>
<label class="form-check-label font-weight-bold" for="is_private">
{{__('Private Account')}}
{{__('settings.privacy.private_account')}}
</label>
<p class="text-muted small help-text">{{__('settings.privacy.when_your_account_is_private_only_people_you_etc')}}</p>
</div>
@ -29,7 +29,7 @@
<div class="form-check pb-3">
<input class="form-check-input" type="checkbox" name="crawlable" id="crawlable" {{!$settings->crawlable ? 'checked=""':''}} {{$settings->is_private ? 'disabled=""':''}}>
<label class="form-check-label font-weight-bold" for="crawlable">
{{__('Disable Search Engine indexing')}}
{{__('settings.privacy.disable_search_engine_indexing')}}
</label>
<p class="text-muted small help-text">{{__('settings.privacy.when_your_account_is_visible_to_search_engines_etc')}} {!! $settings->is_private ? '<strong>'.__('settings.privacy.not_available_when_your_account_is_private').'</strong>' : ''!!}</p>
</div>
@ -37,7 +37,7 @@
<div class="form-check pb-3">
<input class="form-check-input" type="checkbox" name="indexable" id="indexable" {{$profile->indexable ? 'checked=""':''}} {{$settings->is_private ? 'disabled=""':''}}>
<label class="form-check-label font-weight-bold" for="indexable">
{{__('Include public posts in search results')}}
{{__('settings.privacy.include_public_posts_in_search_results')}}
</label>
<p class="text-muted small help-text">{{__('settings.privacy.your_public_posts_may_appear_in_search_results_etc')}} {!! $settings->is_private ? '<strong>'.__('settings.privacy.not_available_when_your_account_is_private').'</strong>' : ''!!}</p>
</div>
@ -46,7 +46,7 @@
<div class="form-check pb-3">
<input class="form-check-input" type="checkbox" name="is_suggestable" id="is_suggestable" {{$settings->is_private ? 'disabled=""':''}} {{auth()->user()->profile->is_suggestable ? 'checked=""':''}}>
<label class="form-check-label font-weight-bold" for="is_suggestable">
{{__('Show on Directory')}}
{{__('settings.privacy.show_on_directory')}}
</label>
<p class="text-muted small help-text">{{__('settings.privacy.when_this_option_is_enabled_your_profile_is_etc')}} {!! $settings->is_private ? '<strong>'.__('settings.privacy.not_available_when_your_account_is_private').'</strong>' : ''!!}</p>
</div>
@ -54,7 +54,7 @@
<div class="form-check pb-3">
<input class="form-check-input" type="checkbox" id="public_dm" {{$settings->public_dm ? 'checked=""':''}} name="public_dm">
<label class="form-check-label font-weight-bold" for="public_dm">
{{__('Receive Direct Messages from anyone')}}
{{__('settings.privacy.receive_direct_messages_from_anyone')}}
</label>
<p class="text-muted small help-text">{{__('settings.privacy.if_selected_you_will_be_able_to_receive_messages_etc')}}</p>
</div>
@ -83,7 +83,7 @@
<div class="form-check pb-3">
<input class="form-check-input" type="checkbox" name="show_profile_follower_count" id="show_profile_follower_count" {{$settings->show_profile_follower_count ? 'checked=""':''}}>
<label class="form-check-label font-weight-bold" for="show_profile_follower_count">
{{__('Show Follower Count')}}
{{__('settings.privacy.show_follower_count')}}
</label>
<p class="text-muted small help-text">{{__('settings.privacy.display_follower_count_on_profile')}}</p>
</div>
@ -92,7 +92,7 @@
<div class="form-check pb-3">
<input class="form-check-input" type="checkbox" name="show_profile_following_count" id="show_profile_following_count" {{$settings->show_profile_following_count ? 'checked=""':''}}>
<label class="form-check-label font-weight-bold" for="show_profile_following_count">
{{__('Show Following Count')}}
{{__('settings.privacy.show_following_count')}}
</label>
<p class="text-muted small help-text">{{__('settings.privacy.display_following_count_on_profile')}}</p>
</div>
@ -100,7 +100,7 @@
<div class="form-check pb-3">
<input class="form-check-input" type="checkbox" name="disable_embeds" id="disable_embeds" {{$settings->disable_embeds ? 'checked=""':''}}>
<label class="form-check-label font-weight-bold" for="disable_embeds">
{{__('Disable Embeds')}}
{{__('settings.privacy.disable_embeds')}}
</label>
<p class="text-muted small help-text">{{__('settings.privacy.disable_post_and_profile_embeds')}}</p>
</div>
@ -109,7 +109,7 @@
<div class="form-check pb-3">
<input class="form-check-input" type="checkbox" name="show_atom" id="show_atom" {{$settings->show_atom ? 'checked=""':''}}>
<label class="form-check-label font-weight-bold" for="show_atom">
{{__('Enable Atom Feed')}}
{{__('settings.privacy.enable_atom_feed')}}
</label>
<p class="text-muted small help-text mb-0">{{__('settings.privacy.enable_your_profile_atom_feed_only_public_profiles_etc')}}</p>
@if($settings->show_atom)

@ -49,7 +49,7 @@
<input type="checkbox" name="check" class="form-control check-all">
</th> --}}
<th scope="col">{{__('settings.relationships.username')}}</th>
<th scope="col">{{__('settings.relationship.action')}}</th>
<th scope="col">{{__('settings.relationships.action')}}</th>
</tr>
</thead>
<tbody>
@ -90,7 +90,7 @@
background-color: #F7FAFC;
}
</style>
@endpush
@endpush
@push('scripts')
<script type="text/javascript">
$(document).ready(() => {

@ -147,6 +147,8 @@ Route::group(['prefix' => 'api'], function () use ($middleware) {
Route::post('statuses/{id}/unreblog', 'Api\ApiV1Controller@statusUnshare')->middleware($middleware);
Route::post('statuses/{id}/bookmark', 'Api\ApiV1Controller@bookmarkStatus')->middleware($middleware);
Route::post('statuses/{id}/unbookmark', 'Api\ApiV1Controller@unbookmarkStatus')->middleware($middleware);
Route::post('statuses/{id}/pin', 'Api\ApiV1Controller@statusPin')->middleware($middleware);
Route::post('statuses/{id}/unpin', 'Api\ApiV1Controller@statusUnpin')->middleware($middleware);
Route::delete('statuses/{id}', 'Api\ApiV1Controller@statusDelete')->middleware($middleware);
Route::get('statuses/{id}', 'Api\ApiV1Controller@statusById')->middleware($middleware);
Route::post('statuses', 'Api\ApiV1Controller@statusCreate')->middleware($middleware);

@ -69,6 +69,8 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact
Route::post('accounts/{id}/block', 'Api\ApiV1Controller@accountBlockById');
Route::post('accounts/{id}/unblock', 'Api\ApiV1Controller@accountUnblockById');
Route::get('statuses/{id}', 'PublicApiController@getStatus');
Route::post('statuses/{id}/pin', 'PublicApiController@statusPin');
Route::post('statuses/{id}/unpin', 'PublicApiController@statusUnpin');
Route::get('accounts/{id}', 'PublicApiController@account');
Route::post('avatar/update', 'ApiController@avatarUpdate');
Route::get('custom_emojis', 'Api\ApiV1Controller@customEmojis');

Loading…
Cancel
Save