Merge branch 'staging' into dev

This commit is contained in:
daniel 2021-10-19 20:41:49 -06:00 committed by GitHub
commit f79486c448
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
55 changed files with 1253 additions and 849 deletions

View File

@ -2,6 +2,22 @@
## [Unreleased](https://github.com/pixelfed/pixelfed/compare/v0.11.1...dev)
### Updated
- Updated NotificationService, fix 500 bug. ([4a609dc3](https://github.com/pixelfed/pixelfed/commit/4a609dc3))
- Updated HttpSignatures, update instance actor headers. Fixes #2935. ([a900de21](https://github.com/pixelfed/pixelfed/commit/a900de21))
- Updated NoteTransformer, fix tag array. ([7b3e672d](https://github.com/pixelfed/pixelfed/commit/7b3e672d))
- Updated video presenters, add playsinline attribute to video tags. ([0299aa5b](https://github.com/pixelfed/pixelfed/commit/0299aa5b))
- Updated RemotePost, RemoteProfile components, add fallback avatars. ([754151dc](https://github.com/pixelfed/pixelfed/commit/754151dc))
- Updated FederationController, move well-known to api middleware and cache webfinger lookups. ([4505d1f0](https://github.com/pixelfed/pixelfed/commit/4505d1f0))
- Updated InstanceActorController, improve json seralization by not escaping slashes. ([0a8eb81b](https://github.com/pixelfed/pixelfed/commit/0a8eb81b))
- Refactor following & relationship logic. Replace FollowerObserver with FollowerService and added RelationshipService to cache results. Removed NotificationTransformer includes and replaced with cached services to improve performance and reduce database queries. ([80d9b939](https://github.com/pixelfed/pixelfed/commit/80d9b939))
- Updated PublicApiController, use AccountService in accountStatuses method. ([bef959f4](https://github.com/pixelfed/pixelfed/commit/bef959f4))
- Updated auth config, add throttle limit to password resets. ([2609c86a](https://github.com/pixelfed/pixelfed/commit/2609c86a))
- Updated StatusCard component, add relationship state button. ([0436b124](https://github.com/pixelfed/pixelfed/commit/0436b124))
- Updated Timeline component, cascade relationship state change. ([f4bd5672](https://github.com/pixelfed/pixelfed/commit/f4bd5672))
- Updated Activity component, only show context button for actionable activities. ([7886fd59](https://github.com/pixelfed/pixelfed/commit/7886fd59))
- ([](https://github.com/pixelfed/pixelfed/commit/))
## [v0.11.1 (2021-09-07)](https://github.com/pixelfed/pixelfed/compare/v0.11.0...v0.11.1)
### Added
- WebP Support ([069a0e4a](https://github.com/pixelfed/pixelfed/commit/069a0e4a))
@ -112,7 +128,6 @@
- Updated DirectMessageController, fix autocomplete bug. ([0f00be4d](https://github.com/pixelfed/pixelfed/commit/0f00be4d))
- Updated StoryService, fix division by zero bug. ([6ae1ba0a](https://github.com/pixelfed/pixelfed/commit/6ae1ba0a))
- Updated ApiV1Controller, fix empty public timeline bug. ([0584f9ee](https://github.com/pixelfed/pixelfed/commit/0584f9ee))
- ([](https://github.com/pixelfed/pixelfed/commit/))
## [v0.11.0 (2021-06-01)](https://github.com/pixelfed/pixelfed/compare/v0.10.10...v0.11.0)
### Added

View File

@ -55,6 +55,7 @@ use App\Services\{
MediaPathService,
PublicTimelineService,
ProfileService,
RelationshipService,
SearchApiV2Service,
StatusService,
MediaBlocklistService
@ -551,7 +552,7 @@ class ApiV1Controller extends Controller
*
* @param array|integer $id
*
* @return \App\Transformer\Api\RelationshipTransformer
* @return \App\Services\RelationshipService
*/
public function accountRelationshipsById(Request $request)
{
@ -563,12 +564,9 @@ class ApiV1Controller extends Controller
]);
$pid = $request->user()->profile_id ?? $request->user()->profile->id;
$ids = collect($request->input('id'));
$filtered = $ids->filter(function($v) use($pid) {
return $v != $pid;
$res = $ids->map(function($id) use($pid) {
return RelationshipService::get($pid, $id);
});
$relations = Profile::whereNull('status')->findOrFail($filtered->values());
$fractal = new Fractal\Resource\Collection($relations, new RelationshipTransformer());
$res = $this->fractal->createData($fractal)->toArray();
return response()->json($res);
}

View File

@ -35,14 +35,14 @@ class FederationController extends Controller
public function nodeinfoWellKnown()
{
abort_if(!config('federation.nodeinfo.enabled'), 404);
return response()->json(Nodeinfo::wellKnown())
return response()->json(Nodeinfo::wellKnown(), 200, [], JSON_UNESCAPED_SLASHES)
->header('Access-Control-Allow-Origin','*');
}
public function nodeinfo()
{
abort_if(!config('federation.nodeinfo.enabled'), 404);
return response()->json(Nodeinfo::get())
return response()->json(Nodeinfo::get(), 200, [], JSON_UNESCAPED_SLASHES)
->header('Access-Control-Allow-Origin','*');
}
@ -53,6 +53,11 @@ class FederationController extends Controller
abort_if(!$request->filled('resource'), 400);
$resource = $request->input('resource');
$hash = hash('sha256', $resource);
$key = 'federation:webfinger:sha256:' . $hash;
if($cached = Cache::get($key)) {
return response()->json($cached, 200, [], JSON_UNESCAPED_SLASHES);
}
$parsed = Nickname::normalizeProfileUrl($resource);
if(empty($parsed) || $parsed['domain'] !== config('pixelfed.domain.app')) {
abort(404);
@ -63,8 +68,9 @@ class FederationController extends Controller
return ProfileController::accountCheck($profile);
}
$webfinger = (new Webfinger($profile))->generate();
Cache::put($key, $webfinger, 43200);
return response()->json($webfinger, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES)
return response()->json($webfinger, 200, [], JSON_UNESCAPED_SLASHES)
->header('Access-Control-Allow-Origin','*');
}

View File

@ -12,6 +12,7 @@ use Auth, Cache;
use Illuminate\Http\Request;
use App\Jobs\FollowPipeline\FollowPipeline;
use App\Util\ActivityPub\Helpers;
use App\Services\FollowerService;
class FollowerController extends Controller
{
@ -70,7 +71,9 @@ class FollowerController extends Controller
]);
if($remote == true && config('federation.activitypub.remoteFollow') == true) {
$this->sendFollow($user, $target);
}
}
FollowerService::add($user->id, $target->id);
} elseif ($private == false && $isFollowing == 0) {
if($user->following()->count() >= Follower::MAX_FOLLOWING) {
abort(400, 'You cannot follow more than ' . Follower::MAX_FOLLOWING . ' accounts');
@ -87,6 +90,7 @@ class FollowerController extends Controller
if($remote == true && config('federation.activitypub.remoteFollow') == true) {
$this->sendFollow($user, $target);
}
FollowerService::add($user->id, $target->id);
FollowPipeline::dispatch($follower);
} else {
if($force == true) {
@ -101,6 +105,7 @@ class FollowerController extends Controller
Follower::whereProfileId($user->id)
->whereFollowingId($target->id)
->delete();
FollowerService::remove($user->id, $target->id);
}
}

View File

@ -15,10 +15,13 @@ use App\Jobs\ImportPipeline\ImportInstagram;
trait Instagram
{
public function instagram()
{
return view('settings.import.instagram.home');
}
public function instagram()
{
if(config_cache('pixelfed.import.instagram.enabled') != true) {
abort(404, 'Feature not enabled');
}
return view('settings.import.instagram.home');
}
public function instagramStart(Request $request)
{

View File

@ -6,8 +6,11 @@ use Illuminate\Http\Request;
trait Mastodon
{
public function mastodon()
{
return view('settings.import.mastodon.home');
}
public function mastodon()
{
if(config_cache('pixelfed.import.instagram.enabled') != true) {
abort(404, 'Feature not enabled');
}
return view('settings.import.mastodon.home');
}
}

View File

@ -11,10 +11,6 @@ class ImportController extends Controller
public function __construct()
{
$this->middleware('auth');
if(config_cache('pixelfed.import.instagram.enabled') != true) {
abort(404, 'Feature not enabled');
}
}
}

View File

@ -12,7 +12,7 @@ class InstanceActorController extends Controller
{
$res = Cache::rememberForever(InstanceActor::PROFILE_KEY, function() {
$res = (new InstanceActor())->first()->getActor();
return json_encode($res);
return json_encode($res, JSON_UNESCAPED_SLASHES);
});
return response($res)->header('Content-Type', 'application/json');
}

View File

@ -11,14 +11,10 @@ use App\Services\FollowerService;
class PollController extends Controller
{
public function __construct()
{
abort_if(!config_cache('instance.polls.enabled'), 404);
}
public function getPoll(Request $request, $id)
{
abort_if(!config_cache('instance.polls.enabled'), 404);
$poll = Poll::findOrFail($id);
$status = Status::findOrFail($poll->status_id);
if($status->scope != 'public') {
@ -34,6 +30,8 @@ class PollController extends Controller
public function vote(Request $request, $id)
{
abort_if(!config_cache('instance.polls.enabled'), 404);
abort_unless($request->user(), 403);
$this->validate($request, [

View File

@ -51,11 +51,11 @@ class PublicApiController extends Controller
protected function getUserData($user)
{
if(!$user) {
return [];
} else {
if(!$user) {
return [];
} else {
return AccountService::get($user->profile_id);
}
}
}
protected function getLikes($status)
@ -94,12 +94,12 @@ class PublicApiController extends Controller
$status = Status::whereProfileId($profile->id)->findOrFail($postid);
$this->scopeCheck($profile, $status);
if(!$request->user()) {
$res = ['status' => StatusService::get($status->id)];
$res = ['status' => StatusService::get($status->id)];
} else {
$item = new Fractal\Resource\Item($status, new StatusStatelessTransformer());
$res = [
'status' => $this->fractal->createData($item)->toArray(),
];
$item = new Fractal\Resource\Item($status, new StatusStatelessTransformer());
$res = [
'status' => $this->fractal->createData($item)->toArray(),
];
}
return response()->json($res);
@ -200,14 +200,14 @@ class PublicApiController extends Controller
public function statusLikes(Request $request, $username, $id)
{
abort_if(!$request->user(), 404);
abort_if(!$request->user(), 404);
$status = Status::findOrFail($id);
$this->scopeCheck($status->profile, $status);
$page = $request->input('page');
if($page && $page >= 3 && $request->user()->profile_id != $status->profile_id) {
return response()->json([
'data' => []
]);
return response()->json([
'data' => []
]);
}
$likes = $this->getLikes($status);
return response()->json([
@ -217,15 +217,15 @@ class PublicApiController extends Controller
public function statusShares(Request $request, $username, $id)
{
abort_if(!$request->user(), 404);
abort_if(!$request->user(), 404);
$profile = Profile::whereUsername($username)->whereNull('status')->firstOrFail();
$status = Status::whereProfileId($profile->id)->findOrFail($id);
$this->scopeCheck($profile, $status);
$page = $request->input('page');
if($page && $page >= 3 && $request->user()->profile_id != $status->profile_id) {
return response()->json([
'data' => []
]);
return response()->json([
'data' => []
]);
}
$shares = $this->getShares($status);
return response()->json([
@ -300,7 +300,7 @@ class PublicApiController extends Controller
'scope',
'local'
)
->where('id', $dir, $id)
->where('id', $dir, $id)
->whereIn('type', ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'])
->whereNotIn('profile_id', $filtered)
->whereLocal(true)
@ -309,7 +309,7 @@ class PublicApiController extends Controller
->limit($limit)
->get()
->map(function($s) use ($user) {
$status = StatusService::get($s->id);
$status = StatusService::getFull($s->id, $user->profile_id);
$status['favourited'] = (bool) LikeService::liked($user->profile_id, $s->id);
return $status;
});
@ -335,16 +335,21 @@ class PublicApiController extends Controller
'reblogs_count',
'updated_at'
)
->whereIn('type', ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'])
->whereIn('type', ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'])
->whereNotIn('profile_id', $filtered)
->with('profile', 'hashtags', 'mentions')
->whereLocal(true)
->whereScope('public')
->orderBy('id', 'desc')
->simplePaginate($limit);
->limit($limit)
->get()
->map(function($s) use ($user) {
$status = StatusService::getFull($s->id, $user->profile_id);
$status['favourited'] = (bool) LikeService::liked($user->profile_id, $s->id);
return $status;
});
$fractal = new Fractal\Resource\Collection($timeline, new StatusTransformer());
$res = $this->fractal->createData($fractal)->toArray();
$res = $timeline->toArray();
}
return response()->json($res);
@ -389,12 +394,12 @@ class PublicApiController extends Controller
});
if($recentFeed == true) {
$key = 'profile:home-timeline-cursor:'.$user->id;
$ttl = now()->addMinutes(30);
$min = Cache::remember($key, $ttl, function() use($pid) {
$res = StatusView::whereProfileId($pid)->orderByDesc('status_id')->first();
return $res ? $res->status_id : null;
});
$key = 'profile:home-timeline-cursor:'.$user->id;
$ttl = now()->addMinutes(30);
$min = Cache::remember($key, $ttl, function() use($pid) {
$res = StatusView::whereProfileId($pid)->orderByDesc('status_id')->first();
return $res ? $res->status_id : null;
});
}
$filtered = $user ? UserFilterService::filters($user->profile_id) : [];
@ -403,16 +408,16 @@ class PublicApiController extends Controller
$textOnlyReplies = false;
if(config('exp.top')) {
$textOnlyPosts = (bool) Redis::zscore('pf:tl:top', $pid);
$textOnlyReplies = (bool) Redis::zscore('pf:tl:replies', $pid);
$textOnlyPosts = (bool) Redis::zscore('pf:tl:top', $pid);
$textOnlyReplies = (bool) Redis::zscore('pf:tl:replies', $pid);
if($textOnlyPosts) {
array_push($types, 'text');
}
if($textOnlyPosts) {
array_push($types, 'text');
}
}
if(config('exp.polls') == true) {
array_push($types, 'poll');
array_push($types, 'poll');
}
if($min || $max) {
@ -438,10 +443,10 @@ class PublicApiController extends Controller
'created_at',
'updated_at'
)
->whereIn('type', $types)
->whereIn('type', $types)
->when($textOnlyReplies != true, function($q, $textOnlyReplies) {
return $q->whereNull('in_reply_to_id');
})
return $q->whereNull('in_reply_to_id');
})
->with('profile', 'hashtags', 'mentions')
->where('id', $dir, $id)
->whereIn('profile_id', $following)
@ -471,10 +476,10 @@ class PublicApiController extends Controller
'created_at',
'updated_at'
)
->whereIn('type', $types)
->when(!$textOnlyReplies, function($q, $textOnlyReplies) {
return $q->whereNull('in_reply_to_id');
})
->whereIn('type', $types)
->when(!$textOnlyReplies, function($q, $textOnlyReplies) {
return $q->whereNull('in_reply_to_id');
})
->with('profile', 'hashtags', 'mentions')
->whereIn('profile_id', $following)
->whereNotIn('profile_id', $filtered)
@ -527,7 +532,7 @@ class PublicApiController extends Controller
'scope',
'created_at',
)
->where('id', $dir, $id)
->where('id', $dir, $id)
->whereNotIn('profile_id', $filtered)
->whereIn('type', ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'])
->whereNotNull('uri')
@ -543,19 +548,19 @@ class PublicApiController extends Controller
});
$res = $timeline->toArray();
} else {
$timeline = Status::select(
'id',
'uri',
'type',
'scope',
'created_at',
)
->whereNotIn('profile_id', $filtered)
->whereIn('type', ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'])
->whereNotNull('uri')
->whereScope('public')
->where('id', '>', $amin)
->orderBy('created_at', 'desc')
$timeline = Status::select(
'id',
'uri',
'type',
'scope',
'created_at',
)
->whereNotIn('profile_id', $filtered)
->whereIn('type', ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'])
->whereNotNull('uri')
->whereScope('public')
->where('id', '>', $amin)
->orderBy('created_at', 'desc')
->limit($limit)
->get()
->map(function($s) use ($user) {
@ -563,7 +568,7 @@ class PublicApiController extends Controller
$status['favourited'] = (bool) LikeService::liked($user->profile_id, $s->id);
return $status;
});
$res = $timeline->toArray();
$res = $timeline->toArray();
}
return response()->json($res);
@ -605,10 +610,10 @@ class PublicApiController extends Controller
return response()->json([]);
}
if(!$profile->domain && !$profile->user->settings->show_profile_followers) {
return response()->json([]);
return response()->json([]);
}
if(!$owner && $request->page > 5) {
return [];
return [];
}
$res = Follower::select('id', 'profile_id', 'following_id')
@ -639,11 +644,11 @@ class PublicApiController extends Controller
abort_if($owner == false && $profile->is_private == true && !$profile->followedBy(Auth::user()->profile), 404);
if(!$profile->domain) {
abort_if($profile->user->settings->show_profile_following == false && $owner == false, 404);
abort_if($profile->user->settings->show_profile_following == false && $owner == false, 404);
}
if(!$owner && $request->page > 5) {
return [];
return [];
}
if($search) {
@ -676,14 +681,15 @@ class PublicApiController extends Controller
]);
$user = $request->user();
$profile = Profile::whereNull('status')->findOrFail($id);
$profile = AccountService::get($id);
abort_if(!$profile, 404);
$limit = $request->limit ?? 9;
$max_id = $request->max_id;
$min_id = $request->min_id;
$scope = ['photo', 'photo:album', 'video', 'video:album'];
if($profile->is_private) {
if($profile['locked']) {
if(!$user) {
return response()->json([]);
}
@ -700,7 +706,7 @@ class PublicApiController extends Controller
$following = Follower::whereProfileId($pid)->pluck('following_id');
return $following->push($pid)->toArray();
});
$visibility = true == in_array($profile->id, $following) ? ['public', 'unlisted', 'private'] : ['public', 'unlisted'];
$visibility = true == in_array($profile['id'], $following) ? ['public', 'unlisted', 'private'] : ['public', 'unlisted'];
} else {
$visibility = ['public', 'unlisted'];
}
@ -708,15 +714,7 @@ class PublicApiController extends Controller
$dir = $min_id ? '>' : '<';
$id = $min_id ?? $max_id;
$res = Status::select(
'id',
'profile_id',
'type',
'scope',
'local',
'created_at'
)
->whereProfileId($profile->id)
$res = Status::whereProfileId($profile['id'])
->whereNull('in_reply_to_id')
->whereNull('reblog_of_id')
->whereIn('type', $scope)
@ -726,18 +724,18 @@ class PublicApiController extends Controller
->orderByDesc('id')
->get()
->map(function($s) use($user) {
try {
$status = StatusService::get($s->id, false);
} catch (\Exception $e) {
$status = false;
}
try {
$status = StatusService::get($s->id, false);
} catch (\Exception $e) {
$status = false;
}
if($user && $status) {
$status['favourited'] = (bool) LikeService::liked($user->profile_id, $s->id);
$status['favourited'] = (bool) LikeService::liked($user->profile_id, $s->id);
}
return $status;
})
->filter(function($s) {
return $s;
return $s;
})
->values();

View File

@ -11,7 +11,7 @@ class InstanceActor extends Model
const PROFILE_BASE = '/i/actor';
const KEY_ID = '/i/actor#main-key';
const PROFILE_KEY = 'federation:_v2:instance:actor:profile';
const PROFILE_KEY = 'federation:_v3:instance:actor:profile';
const PKI_PUBLIC = 'federation:_v1:instance:actor:profile:pki_public';
const PKI_PRIVATE = 'federation:_v1:instance:actor:profile:pki_private';

View File

@ -1,64 +0,0 @@
<?php
namespace App\Observers;
use App\Follower;
use App\Services\FollowerService;
class FollowerObserver
{
/**
* Handle the Follower "created" event.
*
* @param \App\Models\Follower $follower
* @return void
*/
public function created(Follower $follower)
{
FollowerService::add($follower->profile_id, $follower->following_id);
}
/**
* Handle the Follower "updated" event.
*
* @param \App\Models\Follower $follower
* @return void
*/
public function updated(Follower $follower)
{
FollowerService::add($follower->profile_id, $follower->following_id);
}
/**
* Handle the Follower "deleted" event.
*
* @param \App\Models\Follower $follower
* @return void
*/
public function deleted(Follower $follower)
{
FollowerService::remove($follower->profile_id, $follower->following_id);
}
/**
* Handle the Follower "restored" event.
*
* @param \App\Models\Follower $follower
* @return void
*/
public function restored(Follower $follower)
{
FollowerService::add($follower->profile_id, $follower->following_id);
}
/**
* Handle the Follower "force deleted" event.
*
* @param \App\Models\Follower $follower
* @return void
*/
public function forceDeleted(Follower $follower)
{
FollowerService::remove($follower->profile_id, $follower->following_id);
}
}

View File

@ -4,7 +4,6 @@ namespace App\Providers;
use App\Observers\{
AvatarObserver,
FollowerObserver,
LikeObserver,
NotificationObserver,
ModLogObserver,
@ -15,7 +14,6 @@ use App\Observers\{
};
use App\{
Avatar,
Follower,
Like,
Notification,
ModLog,
@ -50,7 +48,6 @@ class AppServiceProvider extends ServiceProvider
StatusHashtag::observe(StatusHashtagObserver::class);
User::observe(UserObserver::class);
UserFilter::observe(UserFilterObserver::class);
Follower::observe(FollowerObserver::class);
Horizon::auth(function ($request) {
return Auth::check() && $request->user()->is_admin;
});

View File

@ -2,7 +2,7 @@
namespace App\Services;
use Zttp\Zttp;
use Illuminate\Support\Facades\Http;
use App\Profile;
use App\Util\ActivityPub\Helpers;
use App\Util\ActivityPub\HttpSignature;
@ -15,14 +15,13 @@ class ActivityPubFetchService
return 0;
}
$headers = HttpSignature::instanceActorSign($url, false, [
'Accept' => 'application/activity+json, application/json',
'User-Agent' => '(Pixelfed/'.config('pixelfed.version').'; +'.config('app.url').')'
]);
$headers = HttpSignature::instanceActorSign($url, false);
$headers['Accept'] = 'application/activity+json, application/json';
$headers['User-Agent'] = '(Pixelfed/'.config('pixelfed.version').'; +'.config('app.url').')';
return Zttp::withHeaders($headers)
return Http::withHeaders($headers)
->timeout(30)
->get($url)
->body();
}
}
}

View File

@ -17,12 +17,14 @@ class FollowerService
public static function add($actor, $target)
{
RelationshipService::refresh($actor, $target);
Redis::zadd(self::FOLLOWING_KEY . $actor, $target, $target);
Redis::zadd(self::FOLLOWERS_KEY . $target, $actor, $actor);
}
public static function remove($actor, $target)
{
RelationshipService::refresh($actor, $target);
Redis::zrem(self::FOLLOWING_KEY . $actor, $target);
Redis::zrem(self::FOLLOWERS_KEY . $target, $actor);
Cache::forget('pf:services:follow:audience:' . $actor);

View File

@ -7,6 +7,13 @@ use App\Instance;
class InstanceService
{
public static function getByDomain($domain)
{
return Cache::remember('pf:services:instance:by_domain:'.$domain, 3600, function() use($domain) {
return Instance::whereDomain($domain)->first();
});
}
public static function getBannedDomains()
{
return Cache::remember('instances:banned:domains', now()->addHours(12), function() {

View File

@ -27,7 +27,10 @@ class NotificationService {
$ids = self::coldGet($id, $start, $stop);
}
foreach($ids as $id) {
$res->push(self::getNotification($id));
$n = self::getNotification($id);
if($n != null) {
$res->push($n);
}
}
return $res;
}
@ -56,7 +59,10 @@ class NotificationService {
$res = collect([]);
foreach($ids as $id) {
$res->push(self::getNotification($id));
$n = self::getNotification($id);
if($n != null) {
$res->push($n);
}
}
return $res->toArray();
}
@ -71,7 +77,10 @@ class NotificationService {
$res = collect([]);
foreach($ids as $id) {
$res->push(self::getNotification($id));
$n = self::getNotification($id);
if($n != null) {
$res->push($n);
}
}
return $res->toArray();
}
@ -129,7 +138,12 @@ class NotificationService {
public static function getNotification($id)
{
return Cache::remember('service:notification:'.$id, now()->addDays(3), function() use($id) {
$n = Notification::with('item')->findOrFail($id);
$n = Notification::with('item')->find($id);
if(!$n) {
return null;
}
$fractal = new Fractal\Manager();
$fractal->setSerializer(new ArraySerializer());
$resource = new Fractal\Resource\Item($n, new NotificationTransformer());

View File

@ -0,0 +1,86 @@
<?php
namespace App\Services;
use Illuminate\Support\Facades\Cache;
use App\Follower;
use App\FollowRequest;
use App\Profile;
use App\UserFilter;
class RelationshipService
{
const CACHE_KEY = 'pf:services:urel:';
public static function get($aid, $tid)
{
$actor = AccountService::get($aid);
$target = AccountService::get($tid);
if(!$actor || !$target) {
return self::defaultRelation($tid);
}
if($actor['id'] === $target['id']) {
return self::defaultRelation($tid);
}
return Cache::remember(self::key("a_{$aid}:t_{$tid}"), 1209600, function() use($aid, $tid) {
return [
'id' => (string) $tid,
'following' => Follower::whereProfileId($aid)->whereFollowingId($tid)->exists(),
'followed_by' => Follower::whereProfileId($tid)->whereFollowingId($aid)->exists(),
'blocking' => UserFilter::whereUserId($aid)
->whereFilterableType('App\Profile')
->whereFilterableId($tid)
->whereFilterType('block')
->exists(),
'muting' => UserFilter::whereUserId($aid)
->whereFilterableType('App\Profile')
->whereFilterableId($tid)
->whereFilterType('mute')
->exists(),
'muting_notifications' => null,
'requested' => FollowRequest::whereFollowerId($aid)
->whereFollowingId($tid)
->exists(),
'domain_blocking' => null,
'showing_reblogs' => null,
'endorsed' => false
];
});
}
public static function delete($aid, $tid)
{
return Cache::forget(self::key("a_{$aid}:t_{$tid}"));
}
public static function refresh($aid, $tid)
{
self::delete($tid, $aid);
self::delete($aid, $tid);
self::get($tid, $aid);
return self::get($aid, $tid);
}
public static function defaultRelation($tid)
{
return [
'id' => (string) $tid,
'following' => false,
'followed_by' => false,
'blocking' => false,
'muting' => false,
'muting_notifications' => null,
'requested' => false,
'domain_blocking' => null,
'showing_reblogs' => null,
'endorsed' => false
];
}
protected static function key($suffix)
{
return self::CACHE_KEY . $suffix;
}
}

View File

@ -40,6 +40,13 @@ class StatusService {
});
}
public static function getFull($id, $pid, $publicOnly = true)
{
$res = self::get($id, $publicOnly);
$res['relationship'] = RelationshipService::get($pid, $res['account']['id']);
return $res;
}
public static function del($id)
{
$status = self::get($id);

View File

@ -35,7 +35,7 @@ class Note extends Fractal\TransformerAbstract
'href' => $parent->permalink(),
'name' => $name
];
$mentions = array_merge($reply, $mentions);
array_push($mentions, $reply);
}
}

View File

@ -2,50 +2,51 @@
namespace App\Transformer\Api;
use App\{
Notification,
Status
};
use App\Notification;
use App\Services\AccountService;
use App\Services\HashidService;
use App\Services\RelationshipService;
use App\Services\StatusService;
use League\Fractal;
class NotificationTransformer extends Fractal\TransformerAbstract
{
protected $defaultIncludes = [
'account',
'status',
'relationship',
'modlog',
'tagged'
];
public function transform(Notification $notification)
{
return [
$res = [
'id' => (string) $notification->id,
'type' => $this->replaceTypeVerb($notification->action),
'created_at' => (string) $notification->created_at->format('c'),
];
}
public function includeAccount(Notification $notification)
{
return $this->item($notification->actor, new AccountTransformer());
}
$n = $notification;
public function includeStatus(Notification $notification)
{
$item = $notification;
if($item->item_id && $item->item_type == 'App\Status') {
$status = Status::with('media')->find($item->item_id);
if($status) {
return $this->item($status, new StatusTransformer());
} else {
return null;
}
} else {
return null;
if($n->actor_id) {
$res['account'] = AccountService::get($n->actor_id);
$res['relationship'] = RelationshipService::get($n->actor_id, $n->profile_id);
}
if($n->item_id && $n->item_type == 'App\Status') {
$res['status'] = StatusService::get($n->item_id, false);
}
if($n->item_id && $n->item_type == 'App\ModLog') {
$ml = $n->item;
$res['modlog'] = [
'id' => $ml->object_uid,
'url' => url('/i/admin/users/modlogs/' . $ml->object_uid)
];
}
if($n->item_id && $n->item_type == 'App\MediaTag') {
$ml = $n->item;
$res['tagged'] = [
'username' => $ml->tagged_username,
'post_url' => '/p/'.HashidService::encode($ml->status_id)
];
}
return $res;
}
public function replaceTypeVerb($verb)
@ -57,56 +58,21 @@ class NotificationTransformer extends Fractal\TransformerAbstract
'reblog' => 'share',
'share' => 'share',
'like' => 'favourite',
'group:like' => 'favourite',
'comment' => 'comment',
'admin.user.modlog.comment' => 'modlog',
'tagged' => 'tagged',
'group:comment' => 'group:comment',
'story:react' => 'story:react',
'story:comment' => 'story:comment'
'story:comment' => 'story:comment',
'group:join:approved' => 'group:join:approved',
'group:join:rejected' => 'group:join:rejected'
];
if(!isset($verbs[$verb])) {
return $verb;
}
return $verbs[$verb];
}
public function includeRelationship(Notification $notification)
{
return $this->item($notification->actor, new RelationshipTransformer());
}
public function includeModlog(Notification $notification)
{
$n = $notification;
if($n->item_id && $n->item_type == 'App\ModLog') {
$ml = $n->item;
if(!empty($ml)) {
$res = $this->item($ml, function($ml) {
return [
'id' => $ml->object_uid,
'url' => url('/i/admin/users/modlogs/' . $ml->object_uid)
];
});
return $res;
} else {
return null;
}
} else {
return null;
}
}
public function includeTagged(Notification $notification)
{
$n = $notification;
if($n->item_id && $n->item_type == 'App\MediaTag') {
$ml = $n->item;
$res = $this->item($ml, function($ml) {
return [
'username' => $ml->tagged_username,
'post_url' => '/p/'.HashidService::encode($ml->status_id)
];
});
return $res;
} else {
return null;
}
}
}

View File

@ -43,7 +43,7 @@ class HttpSignature {
$digest = self::_digest($body);
}
$headers = self::_headersToSign($url, $body ? $digest : false);
$headers = array_unique(array_merge($headers, $addlHeaders));
$headers = array_merge($headers, $addlHeaders);
$stringToSign = self::_headersToSigningString($headers);
$signedHeaders = implode(' ', array_map('strtolower', array_keys($headers)));
$key = openssl_pkey_get_private($privateKey);
@ -133,7 +133,6 @@ class HttpSignature {
'Date' => $date->format('D, d M Y H:i:s \G\M\T'),
'Host' => parse_url($url, PHP_URL_HOST),
'Accept' => 'application/activity+json, application/json',
'Content-Type' => 'application/activity+json'
];
if($digest) {

View File

@ -455,6 +455,7 @@ class Inbox
Cache::forget('profile:follower_count:'.$actor->id);
Cache::forget('profile:following_count:'.$target->id);
Cache::forget('profile:following_count:'.$actor->id);
FollowerService::add($actor->id, $target->id);
} else {
$follower = new Follower;
@ -464,6 +465,7 @@ class Inbox
$follower->save();
FollowPipeline::dispatch($follower);
FollowerService::add($actor->id, $target->id);
// send Accept to remote profile
$accept = [
@ -722,6 +724,7 @@ class Inbox
->whereItemId($following->id)
->whereItemType('App\Profile')
->forceDelete();
FollowerService::remove($profile->id, $following->id);
break;
case 'Like':

View File

@ -10,34 +10,29 @@ class Nodeinfo {
public static function get()
{
$res = Cache::remember('api:nodeinfo', now()->addMinutes(15), function () {
$activeHalfYear = Cache::remember('api:nodeinfo:ahy', now()->addHours(12), function() {
// todo: replace with last_active_at after July 9, 2021 (96afc3e781)
$count = collect([]);
$likes = Like::select('profile_id')->with('actor')->where('created_at', '>', now()->subMonths(6)->toDateTimeString())->groupBy('profile_id')->get()->filter(function($like) {return $like->actor && $like->actor->domain == null;})->pluck('profile_id')->toArray();
$count = $count->merge($likes);
$statuses = Status::select('profile_id')->whereLocal(true)->where('created_at', '>', now()->subMonths(6)->toDateTimeString())->groupBy('profile_id')->pluck('profile_id')->toArray();
$count = $count->merge($statuses);
$profiles = User::select('profile_id', 'last_active_at')
->whereNotNull('last_active_at')
$res = Cache::remember('api:nodeinfo', 300, function () {
$activeHalfYear = Cache::remember('api:nodeinfo:ahy', 172800, function() {
return User::select('last_active_at')
->where('last_active_at', '>', now()->subMonths(6))
->pluck('profile_id')
->toArray();
$newProfiles = User::select('profile_id', 'last_active_at', 'created_at')
->whereNull('last_active_at')
->where('created_at', '>', now()->subMonths(6))
->pluck('profile_id')
->toArray();
$count = $count->merge($newProfiles);
$count = $count->merge($profiles);
return $count->unique()->count();
->orWhere('created_at', '>', now()->subMonths(6))
->count();
});
$activeMonth = Cache::remember('api:nodeinfo:am', now()->addHours(2), function() {
$activeMonth = Cache::remember('api:nodeinfo:am', 172800, function() {
return User::select('last_active_at')
->where('last_active_at', '>', now()->subMonths(1))
->orWhere('created_at', '>', now()->subMonths(1))
->count();
});
$users = Cache::remember('api:nodeinfo:users', 43200, function() {
return User::count();
});
$statuses = Cache::remember('api:nodeinfo:statuses', 21600, function() {
return Status::whereLocal(true)->count();
});
return [
'metadata' => [
'nodeName' => config_cache('app.name'),
@ -59,10 +54,10 @@ class Nodeinfo {
'version' => config('pixelfed.version'),
],
'usage' => [
'localPosts' => Status::whereLocal(true)->count(),
'localPosts' => $statuses,
'localComments' => 0,
'users' => [
'total' => User::count(),
'total' => $users,
'activeHalfyear' => (int) $activeHalfYear,
'activeMonth' => (int) $activeMonth,
],

1261
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -96,6 +96,7 @@ return [
'provider' => 'users',
'table' => 'password_resets',
'expire' => 60,
'throttle' => 60,
],
],

File diff suppressed because one or more lines are too long

2
public/js/admin.js vendored

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

2
public/js/direct.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

2
public/js/rempos.js vendored

File diff suppressed because one or more lines are too long

2
public/js/rempro.js vendored

File diff suppressed because one or more lines are too long

2
public/js/status.js vendored

File diff suppressed because one or more lines are too long

View File

@ -1 +1 @@
(window.webpackJsonp=window.webpackJsonp||[]).push([[27],{15:function(e,a,o){e.exports=o("YMO/")},"YMO/":function(e,a,o){(function(e){function o(e){return(o="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}ace.define("ace/theme/monokai",["require","exports","module","ace/lib/dom"],(function(e,a,o){a.isDark=!0,a.cssClass="ace-monokai",a.cssText=".ace-monokai .ace_gutter {background: #2F3129;color: #8F908A}.ace-monokai .ace_print-margin {width: 1px;background: #555651}.ace-monokai {background-color: #272822;color: #F8F8F2}.ace-monokai .ace_cursor {color: #F8F8F0}.ace-monokai .ace_marker-layer .ace_selection {background: #49483E}.ace-monokai.ace_multiselect .ace_selection.ace_start {box-shadow: 0 0 3px 0px #272822;}.ace-monokai .ace_marker-layer .ace_step {background: rgb(102, 82, 0)}.ace-monokai .ace_marker-layer .ace_bracket {margin: -1px 0 0 -1px;border: 1px solid #49483E}.ace-monokai .ace_marker-layer .ace_active-line {background: #202020}.ace-monokai .ace_gutter-active-line {background-color: #272727}.ace-monokai .ace_marker-layer .ace_selected-word {border: 1px solid #49483E}.ace-monokai .ace_invisible {color: #52524d}.ace-monokai .ace_entity.ace_name.ace_tag,.ace-monokai .ace_keyword,.ace-monokai .ace_meta.ace_tag,.ace-monokai .ace_storage {color: #F92672}.ace-monokai .ace_punctuation,.ace-monokai .ace_punctuation.ace_tag {color: #fff}.ace-monokai .ace_constant.ace_character,.ace-monokai .ace_constant.ace_language,.ace-monokai .ace_constant.ace_numeric,.ace-monokai .ace_constant.ace_other {color: #AE81FF}.ace-monokai .ace_invalid {color: #F8F8F0;background-color: #F92672}.ace-monokai .ace_invalid.ace_deprecated {color: #F8F8F0;background-color: #AE81FF}.ace-monokai .ace_support.ace_constant,.ace-monokai .ace_support.ace_function {color: #66D9EF}.ace-monokai .ace_fold {background-color: #A6E22E;border-color: #F8F8F2}.ace-monokai .ace_storage.ace_type,.ace-monokai .ace_support.ace_class,.ace-monokai .ace_support.ace_type {font-style: italic;color: #66D9EF}.ace-monokai .ace_entity.ace_name.ace_function,.ace-monokai .ace_entity.ace_other,.ace-monokai .ace_entity.ace_other.ace_attribute-name,.ace-monokai .ace_variable {color: #A6E22E}.ace-monokai .ace_variable.ace_parameter {font-style: italic;color: #FD971F}.ace-monokai .ace_string {color: #E6DB74}.ace-monokai .ace_comment {color: #75715E}.ace-monokai .ace_indent-guide {background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAEklEQVQImWPQ0FD0ZXBzd/wPAAjVAoxeSgNeAAAAAElFTkSuQmCC) right repeat-y}",e("../lib/dom").importCssString(a.cssText,a.cssClass)})),ace.require(["ace/theme/monokai"],(function(c){"object"==o(e)&&"object"==o(a)&&e&&(e.exports=c)}))}).call(this,o("YuTi")(e))},YuTi:function(e,a){e.exports=function(e){return e.webpackPolyfill||(e.deprecate=function(){},e.paths=[],e.children||(e.children=[]),Object.defineProperty(e,"loaded",{enumerable:!0,get:function(){return e.l}}),Object.defineProperty(e,"id",{enumerable:!0,get:function(){return e.i}}),e.webpackPolyfill=1),e}}},[[15,0]]]);
(window.webpackJsonp=window.webpackJsonp||[]).push([[25],{15:function(e,a,o){e.exports=o("YMO/")},"YMO/":function(e,a,o){(function(e){function o(e){return(o="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}ace.define("ace/theme/monokai",["require","exports","module","ace/lib/dom"],(function(e,a,o){a.isDark=!0,a.cssClass="ace-monokai",a.cssText=".ace-monokai .ace_gutter {background: #2F3129;color: #8F908A}.ace-monokai .ace_print-margin {width: 1px;background: #555651}.ace-monokai {background-color: #272822;color: #F8F8F2}.ace-monokai .ace_cursor {color: #F8F8F0}.ace-monokai .ace_marker-layer .ace_selection {background: #49483E}.ace-monokai.ace_multiselect .ace_selection.ace_start {box-shadow: 0 0 3px 0px #272822;}.ace-monokai .ace_marker-layer .ace_step {background: rgb(102, 82, 0)}.ace-monokai .ace_marker-layer .ace_bracket {margin: -1px 0 0 -1px;border: 1px solid #49483E}.ace-monokai .ace_marker-layer .ace_active-line {background: #202020}.ace-monokai .ace_gutter-active-line {background-color: #272727}.ace-monokai .ace_marker-layer .ace_selected-word {border: 1px solid #49483E}.ace-monokai .ace_invisible {color: #52524d}.ace-monokai .ace_entity.ace_name.ace_tag,.ace-monokai .ace_keyword,.ace-monokai .ace_meta.ace_tag,.ace-monokai .ace_storage {color: #F92672}.ace-monokai .ace_punctuation,.ace-monokai .ace_punctuation.ace_tag {color: #fff}.ace-monokai .ace_constant.ace_character,.ace-monokai .ace_constant.ace_language,.ace-monokai .ace_constant.ace_numeric,.ace-monokai .ace_constant.ace_other {color: #AE81FF}.ace-monokai .ace_invalid {color: #F8F8F0;background-color: #F92672}.ace-monokai .ace_invalid.ace_deprecated {color: #F8F8F0;background-color: #AE81FF}.ace-monokai .ace_support.ace_constant,.ace-monokai .ace_support.ace_function {color: #66D9EF}.ace-monokai .ace_fold {background-color: #A6E22E;border-color: #F8F8F2}.ace-monokai .ace_storage.ace_type,.ace-monokai .ace_support.ace_class,.ace-monokai .ace_support.ace_type {font-style: italic;color: #66D9EF}.ace-monokai .ace_entity.ace_name.ace_function,.ace-monokai .ace_entity.ace_other,.ace-monokai .ace_entity.ace_other.ace_attribute-name,.ace-monokai .ace_variable {color: #A6E22E}.ace-monokai .ace_variable.ace_parameter {font-style: italic;color: #FD971F}.ace-monokai .ace_string {color: #E6DB74}.ace-monokai .ace_comment {color: #75715E}.ace-monokai .ace_indent-guide {background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAEklEQVQImWPQ0FD0ZXBzd/wPAAjVAoxeSgNeAAAAAElFTkSuQmCC) right repeat-y}",e("../lib/dom").importCssString(a.cssText,a.cssClass)})),ace.require(["ace/theme/monokai"],(function(c){"object"==o(e)&&"object"==o(a)&&e&&(e.exports=c)}))}).call(this,o("YuTi")(e))},YuTi:function(e,a){e.exports=function(e){return e.webpackPolyfill||(e.deprecate=function(){},e.paths=[],e.children||(e.children=[]),Object.defineProperty(e,"loaded",{enumerable:!0,get:function(){return e.l}}),Object.defineProperty(e,"id",{enumerable:!0,get:function(){return e.i}}),e.webpackPolyfill=1),e}}},[[15,0]]]);

File diff suppressed because one or more lines are too long

2
public/js/vendor.js vendored

File diff suppressed because one or more lines are too long

View File

@ -1,10 +1,10 @@
{
"/js/manifest.js": "/js/manifest.js?id=7db827d654313dce4250",
"/js/vendor.js": "/js/vendor.js?id=c1d209d56a681363a028",
"/js/vendor.js": "/js/vendor.js?id=4736aa0fc51d85c921c6",
"/js/ace.js": "/js/ace.js?id=11e5550a450fece75c33",
"/js/activity.js": "/js/activity.js?id=c6dbcb16ae7e086e2695",
"/js/admin.js": "/js/admin.js?id=8d48022b71fa67e40d80",
"/js/app.js": "/js/app.js?id=f48d6aa0901c53c13ee0",
"/js/activity.js": "/js/activity.js?id=ccc3329aacb23a16d0d0",
"/js/admin.js": "/js/admin.js?id=bc1542fd4aa4e71daad7",
"/js/app.js": "/js/app.js?id=13bdbfb3f3f2c205c0ac",
"/css/app.css": "/css/app.css?id=de797b5388be16568353",
"/css/appdark.css": "/css/appdark.css?id=3da08a3e37579777460a",
"/css/admin.css": "/css/admin.css?id=ef822512ab2cfacef881",
@ -12,25 +12,25 @@
"/css/quill.css": "/css/quill.css?id=ece45380f676947dd7f8",
"/js/collectioncompose.js": "/js/collectioncompose.js?id=6678ddbea8c68875f830",
"/js/collections.js": "/js/collections.js?id=bdd652ffb9754adf81aa",
"/js/components.js": "/js/components.js?id=7bd6746b7213ee1cfa07",
"/js/components.js": "/js/components.js?id=fba10d02a472976640ed",
"/js/compose.js": "/js/compose.js?id=0dd3798521b8b0ae7b7b",
"/js/compose-classic.js": "/js/compose-classic.js?id=c994899e82ff5a905c81",
"/js/developers.js": "/js/developers.js?id=9bba3be10491e58de952",
"/js/direct.js": "/js/direct.js?id=2e34b51be7aa0b65d7e7",
"/js/direct.js": "/js/direct.js?id=451f2ad256090f24cced",
"/js/discover.js": "/js/discover.js?id=dcc903e7b89832d4fa59",
"/js/hashtag.js": "/js/hashtag.js?id=1ec0defe0d591a4a417e",
"/js/loops.js": "/js/loops.js?id=ae34b77c4cfe1824f5a0",
"/js/memoryprofile.js": "/js/memoryprofile.js?id=e20bf2a59c5003f56b97",
"/js/memoryprofile.js": "/js/memoryprofile.js?id=887eab75f4812600de26",
"/js/mode-dot.js": "/js/mode-dot.js?id=af90766521763f9d8f71",
"/js/profile.js": "/js/profile.js?id=3dcca2e2eea64488f62d",
"/js/profile.js": "/js/profile.js?id=d3b094b75a4e7c10f794",
"/js/profile-directory.js": "/js/profile-directory.js?id=e63d5f2c6f2d5710a8bd",
"/js/quill.js": "/js/quill.js?id=4769f11fc9a6c32dde50",
"/js/rempos.js": "/js/rempos.js?id=010cf1d2a75deb514060",
"/js/rempro.js": "/js/rempro.js?id=4608c76dfbfb77f259a7",
"/js/rempos.js": "/js/rempos.js?id=a4705303116584cfa3f2",
"/js/rempro.js": "/js/rempro.js?id=a498e954ac466ac3ad61",
"/js/search.js": "/js/search.js?id=68156d01717856b142ea",
"/js/status.js": "/js/status.js?id=a9ea8019f0891c03452b",
"/js/status.js": "/js/status.js?id=c41642dc3bcae66c96ab",
"/js/stories.js": "/js/stories.js?id=94721cbf86251179e7f7",
"/js/story-compose.js": "/js/story-compose.js?id=34b9e22a404107475f2e",
"/js/theme-monokai.js": "/js/theme-monokai.js?id=498da56ab16309f277e7",
"/js/timeline.js": "/js/timeline.js?id=21857714f1b6b5a1bc78"
"/js/theme-monokai.js": "/js/theme-monokai.js?id=8842103833ba4861bcfa",
"/js/timeline.js": "/js/timeline.js?id=63de948e4a82abac8a87"
}

View File

@ -103,31 +103,31 @@ window.App.util = {
}
return Math.floor(seconds) + "s";
}),
timeAhead: (function(ts) {
timeAhead: (function(ts, short = true) {
let date = Date.parse(ts);
let diff = date - Date.parse(new Date());
let seconds = Math.floor((diff) / 1000);
let interval = Math.floor(seconds / 63072000);
if (interval >= 1) {
return interval + "y";
return interval + (short ? "y" : " years");
}
interval = Math.floor(seconds / 604800);
if (interval >= 1) {
return interval + "w";
return interval + (short ? "w" : " weeks");
}
interval = Math.floor(seconds / 86400);
if (interval >= 1) {
return interval + "d";
return interval + (short ? "d" : " days");
}
interval = Math.floor(seconds / 3600);
if (interval >= 1) {
return interval + "h";
return interval + (short ? "h" : " hours");
}
interval = Math.floor(seconds / 60);
if (interval >= 1) {
return interval + "m";
return interval + (short ? "m" : " minutes");
}
return Math.floor(seconds) + "s";
return Math.floor(seconds) + (short ? "s" : " seconds");
}),
rewriteLinks: (function(i) {
@ -234,7 +234,8 @@ window.App.util = {
'filter-willow': 'brightness(1.2) contrast(.85) saturate(.05) sepia(.2)',
'filter-xpro-ii': 'sepia(.45) contrast(1.25) brightness(1.75) saturate(1.3) hue-rotate(-5deg)'
},
emoji: ['😂','💯','❤️','🙌','👏','👌','😍','😯','😢','😅','😁','🙂','😎','😀','🤣','😃','😄','😆','😉','😊','😋','😘','😗','😙','😚','🤗','🤩','🤔','🤨','😐','😑','😶','🙄','😏','😣','😥','😮','🤐','😪','😫','😴','😌','😛','😜','😝','🤤','😒','😓','😔','😕','🙃','🤑','😲','🙁','😖','😞','😟','😤','😭','😦','😧','😨','😩','🤯','😬','😰','😱','😳','🤪','😵','😡','😠','🤬','😷','🤒','🤕','🤢','🤮','🤧','😇','🤠','🤡','🤥','🤫','🤭','🧐','🤓','😈','👿','👹','👺','💀','👻','👽','🤖','💩','😺','😸','😹','😻','😼','😽','🙀','😿','😾','🤲','👐','🤝','👍','👎','👊','✊','🤛','🤜','🤞','✌️','🤟','🤘','👈','👉','👆','👇','☝️','✋','🤚','🖐','🖖','👋','🤙','💪','🖕','✍️','🙏','💍','💄','💋','👄','👅','👂','👃','👣','👁','👀','🧠','🗣','👤','👥'
emoji: [
'😂','💯','❤️','🙌','👏','👌','😍','😯','😢','😅','😁','🙂','😎','😀','🤣','😃','😄','😆','😉','😊','😋','😘','😗','😙','😚','🤗','🤩','🤔','🤨','😐','😑','😶','🙄','😏','😣','😥','😮','🤐','😪','😫','😴','😌','😛','😜','😝','🤤','😒','😓','😔','😕','🙃','🤑','😲','🙁','😖','😞','😟','😤','😭','😦','😧','😨','😩','🤯','😬','😰','😱','😳','🤪','😵','😡','😠','🤬','😷','🤒','🤕','🤢','🤮','🤧','😇','🤠','🤡','🤥','🤫','🤭','🧐','🤓','😈','👿','👹','👺','💀','👻','👽','🤖','💩','😺','😸','😹','😻','😼','😽','🙀','😿','😾','🤲','👐','🤝','👍','👎','👊','✊','🤛','🤜','🤞','✌️','🤟','🤘','👈','👉','👆','👇','☝️','✋','🤚','🖐','🖖','👋','🤙','💪','🖕','✍️','🙏','💍','💄','💋','👄','👅','👂','👃','👣','👁','👀','🧠','🗣','👤','👥'
],
embed: {
post: (function(url, caption = true, likes = false, layout = 'full') {

View File

@ -22,56 +22,60 @@
<a :href="getProfileUrl(n.account)" class="font-weight-bold text-dark word-break" data-placement="bottom" data-toggle="tooltip" :title="n.account.username">{{n.account.local == false ? '@':''}}{{truncate(n.account.username)}}</a> liked your <a class="font-weight-bold" v-bind:href="getPostUrl(n.status)">post</a>.
</p>
</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" data-placement="bottom" data-toggle="tooltip" :title="n.account.username">{{n.account.local == false ? '@':''}}{{truncate(n.account.username)}}</a> commented on your <a class="font-weight-bold" v-bind:href="getPostUrl(n.status)">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.username">{{n.account.local == false ? '@':''}}{{truncate(n.account.username)}}</a> commented on your <a class="font-weight-bold" v-bind:href="n.group_post_url">group post</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.username">{{n.account.local == false ? '@':''}}{{truncate(n.account.username)}}</a> reacted to your <a class="font-weight-bold" v-bind:href="'/account/direct/t/'+n.account.id">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.username">{{n.account.local == false ? '@':''}}{{truncate(n.account.username)}}</a> commented on your <a class="font-weight-bold" v-bind:href="'/account/direct/t/'+n.account.id">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" data-placement="bottom" data-toggle="tooltip" :title="n.account.username">{{n.account.local == false ? '@':''}}{{truncate(n.account.username)}}</a> <a class="font-weight-bold" v-bind:href="mentionUrl(n.status)">mentioned</a> 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" data-placement="bottom" data-toggle="tooltip" :title="n.account.username">{{n.account.local == false ? '@':''}}{{truncate(n.account.username)}}</a> followed 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" data-placement="bottom" data-toggle="tooltip" :title="n.account.username">{{n.account.local == false ? '@':''}}{{truncate(n.account.username)}}</a> shared your <a class="font-weight-bold" v-bind:href="getPostUrl(n.status)">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.username">{{truncate(n.account.username)}}</a> updated a <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.username">{{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>.
</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.username">{{n.account.local == false ? '@':''}}{{truncate(n.account.username)}}</a> sent a <a class="font-weight-bold" v-bind:href="'/account/direct/t/'+n.account.id">dm</a>.
</p>
</div>
<div class="align-items-center">
<span class="small text-muted" data-toggle="tooltip" data-placement="bottom" :title="n.created_at">{{timeAgo(n.created_at)}}</span>
</div>
@ -105,7 +109,7 @@
</a>
</div> -->
<div v-else>
<a class="btn btn-outline-primary py-0 font-weight-bold" :href="viewContext(n)">View</a>
<a v-if="viewContext(n) != '/'" class="btn btn-outline-primary py-0 font-weight-bold" :href="viewContext(n)">View</a>
</div>
</div>
</div>
@ -209,6 +213,9 @@ export default {
}
return true;
});
let ids = data.map(n => n.id);
this.notificationMaxId = Math.max(...ids);
this.notifications.push(...data);
this.notificationCursor++;
$state.loaded();

View File

@ -35,7 +35,7 @@
<div class="d-flex d-md-none align-items-center justify-content-between card-header bg-white w-100">
<div class="d-flex">
<div class="status-avatar mr-2" @click="redirect(profileUrl)">
<img :src="statusAvatar" width="24px" height="24px" style="border-radius:12px;" class="cursor-pointer">
<img :src="statusAvatar" width="24px" height="24px" style="border-radius:12px;" class="cursor-pointer" onerror="this.onerror=null;this.src='/storage/avatars/default.jpg?v=0';">
</div>
<div class="username">
<span class="username-link font-weight-bold text-dark cursor-pointer" @click="redirect(profileUrl)">{{ statusUsername }}</span>
@ -94,7 +94,7 @@
<div class="d-md-flex d-none align-items-center justify-content-between card-header py-3 bg-white">
<div class="d-flex align-items-center status-username text-truncate">
<div class="status-avatar mr-2" @click="redirect(profileUrl)">
<img :src="statusAvatar" width="24px" height="24px" style="border-radius:12px;" class="cursor-pointer">
<img :src="statusAvatar" width="24px" height="24px" style="border-radius:12px;" class="cursor-pointer" onerror="this.onerror=null;this.src='/storage/avatars/default.jpg?v=0';">
</div>
<div class="username">
<span class="username-link font-weight-bold text-dark cursor-pointer" @click="redirect(profileUrl)">{{ statusUsername }}</span>
@ -157,7 +157,7 @@
</p>
<div class="comments mt-3">
<div v-for="(reply, index) in results" class="pb-4 media" :key="'tl' + reply.id + '_' + index">
<img :src="reply.account.avatar" class="rounded-circle border mr-3" width="42px" height="42px">
<img :src="reply.account.avatar" class="rounded-circle border mr-3" width="42px" height="42px" onerror="this.onerror=null;this.src='/storage/avatars/default.jpg?v=0';">
<div class="media-body">
<div v-if="reply.sensitive == true">
<span class="py-3">
@ -190,7 +190,7 @@
</div>
<div v-if="reply.thread == true" class="comment-thread">
<div v-for="(s, sindex) in reply.replies" class="pb-3 media" :key="'cr' + s.id + '_' + index">
<img :src="s.account.avatar" class="rounded-circle border mr-3" width="25px" height="25px">
<img :src="s.account.avatar" class="rounded-circle border mr-3" width="25px" height="25px" onerror="this.onerror=null;this.src='/storage/avatars/default.jpg?v=0';">
<div class="media-body">
<p class="d-flex justify-content-between align-items-top read-more" style="overflow-y: hidden;">
<span>
@ -315,7 +315,7 @@
<div class="list-group-item border-0 py-1" v-for="(user, index) in likes" :key="'modal_likes_'+index">
<div class="media">
<a :href="user.url">
<img class="mr-3 rounded-circle box-shadow" :src="user.avatar" :alt="user.username + 's avatar'" width="30px">
<img class="mr-3 rounded-circle box-shadow" :src="user.avatar" :alt="user.username + 's avatar'" width="30px" onerror="this.onerror=null;this.src='/storage/avatars/default.jpg?v=0';">
</a>
<div class="media-body">
<p class="mb-0" style="font-size: 14px">
@ -348,7 +348,7 @@
<div class="list-group-item border-0 py-1" v-for="(user, index) in shares" :key="'modal_shares_'+index">
<div class="media">
<a :href="user.url">
<img class="mr-3 rounded-circle box-shadow" :src="user.avatar" :alt="user.username + 's avatar'" width="30px">
<img class="mr-3 rounded-circle box-shadow" :src="user.avatar" :alt="user.username + 's avatar'" width="30px" onerror="this.onerror=null;this.src='/storage/avatars/default.jpg?v=0';">
</a>
<div class="media-body">
<div class="d-inline-block">
@ -382,7 +382,7 @@
<div class="list-group-item border-0 py-1" v-for="(taguser, index) in status.taggedPeople" :key="'modal_taggedpeople_'+index">
<div class="media">
<a :href="'/'+taguser.username">
<img class="mr-3 rounded-circle box-shadow" :src="taguser.avatar" :alt="taguser.username + 's avatar'" width="30px">
<img class="mr-3 rounded-circle box-shadow" :src="taguser.avatar" :alt="taguser.username + 's avatar'" width="30px" onerror="this.onerror=null;this.src='/storage/avatars/default.jpg?v=0';">
</a>
<div class="media-body">
<p class="pt-1 d-flex justify-content-between" style="font-size: 14px">

View File

@ -19,7 +19,7 @@
</div>
<div class="card-body pb-0">
<div class="mt-n5 mb-3">
<img class="rounded-circle p-1 border mt-n4 bg-white shadow" :src="profile.avatar" width="90px" height="90px;">
<img class="rounded-circle p-1 border mt-n4 bg-white shadow" :src="profile.avatar" width="90px" height="90px;" onerror="this.onerror=null;this.src='/storage/avatars/default.jpg?v=0';">
<span class="float-right mt-n1">
<span>
<button v-if="relationship && relationship.following == false" class="btn btn-outline-light py-0 px-3 mt-n1" style="font-size:13px; font-weight: 500;" @click="followProfile();">Follow</button>

View File

@ -12,7 +12,7 @@
<div style="margin-top:-2px;">
<story-component v-if="config.features.stories" :scope="scope"></story-component>
</div>
<div>
<div class="pt-4">
<div v-if="loading" class="text-center" style="padding-top:10px;">
<div class="spinner-border" role="status">
<span class="sr-only">Loading...</span>
@ -106,6 +106,8 @@
size="small"
v-on:status-delete="deleteStatus"
v-on:comment-focus="commentFocus"
v-on:followed="followedAccount"
v-on:unfollowed="unfollowedAccount"
/>
</div>
@ -1067,7 +1069,29 @@
this.feed = this.feed.filter(s => {
return s.id != status;
});
}
},
followedAccount(id) {
this.feed = this.feed.map(s => {
if(s.account.id == id) {
if(s.hasOwnProperty('relationship') && s.relationship.following == false) {
s.relationship.following = true;
}
}
return s;
});
},
unfollowedAccount(id) {
this.feed = this.feed.map(s => {
if(s.account.id == id) {
if(s.hasOwnProperty('relationship') && s.relationship.following == true) {
s.relationship.following = false;
}
}
return s;
});
},
},
beforeDestroy () {

View File

@ -71,6 +71,14 @@
<a v-if="status.place" class="small text-decoration-none text-muted" :href="'/discover/places/'+status.place.id+'/'+status.place.slug" title="Location" data-toggle="tooltip"><i class="fas fa-map-marked-alt"></i> {{status.place.name}}, {{status.place.country}}</a>
</div>
</div>
<div v-if="canFollow(status)">
<span class="px-2"></span>
<button class="btn btn-primary btn-sm font-weight-bold py-1 px-3 rounded-lg" @click="follow(status.account.id)"><i class="far fa-user-plus mr-1"></i> Follow</button>
</div>
<div v-if="status.hasOwnProperty('relationship') && status.relationship.hasOwnProperty('following') && status.relationship.following">
<span class="px-2"></span>
<button class="btn btn-outline-primary btn-sm font-weight-bold py-1 px-3 rounded-lg" @click="unfollow(status.account.id)"><i class="far fa-user-check mr-1"></i> Following</button>
</div>
<div class="text-right" style="flex-grow:1;">
<button class="btn btn-link text-dark py-0" type="button" @click="ctxMenu()">
<span class="fas fa-ellipsis-h text-lighter"></span>
@ -382,6 +390,52 @@
statusDeleted(status) {
this.$emit('status-delete', status);
},
canFollow(status) {
if(!status.hasOwnProperty('relationship')) {
return false;
}
if(!status.hasOwnProperty('account') || !status.account.hasOwnProperty('id')) {
return false;
}
if(status.account.id === this.profile.id) {
return false;
}
return !status.relationship.following;
},
follow(id) {
event.currentTarget.blur();
axios.post('/i/follow', {
item: id
}).then(res => {
this.status.relationship.following = true;
this.$emit('followed', id);
}).catch(err => {
if(err.response.data.message) {
swal('Error', err.response.data.message, 'error');
}
});
},
unfollow(id) {
event.currentTarget.blur();
axios.post('/i/follow', {
item: id
}).then(res => {
this.status.relationship.following = false;
this.$emit('unfollowed', id);
}).catch(err => {
if(err.response.data.message) {
swal('Error', err.response.data.message, 'error');
}
});
}
}
}

View File

@ -14,7 +14,7 @@
>
<b-carousel-slide v-for="(media, index) in status.media_attachments" :key="media.id + '-media'">
<video v-if="media.type == 'Video'" slot="img" class="embed-responsive-item" preload="none" controls loop :alt="media.description" width="100%" height="100%" :poster="media.preview_url">
<video v-if="media.type == 'Video'" slot="img" class="embed-responsive-item" preload="none" controls playsinline loop :alt="media.description" width="100%" height="100%" :poster="media.preview_url">
<source :src="media.url" :type="media.mime">
</video>
@ -72,4 +72,4 @@
export default {
props: ['status']
}
</script>
</script>

View File

@ -13,7 +13,7 @@
:interval="0"
>
<b-carousel-slide v-for="(vid, index) in status.media_attachments" :key="vid.id + '-media'">
<video slot="img" class="embed-responsive-item" preload="none" controls loop :alt="vid.description" width="100%" height="100%" :poster="vid.preview_url">
<video slot="img" class="embed-responsive-item" preload="none" controls playsinline loop :alt="vid.description" width="100%" height="100%" :poster="vid.preview_url">
<source :src="vid.url" :type="vid.mime">
</video>
</b-carousel-slide>
@ -29,7 +29,7 @@
:interval="0"
>
<b-carousel-slide v-for="(vid, index) in status.media_attachments" :key="vid.id + '-media'">
<video slot="img" class="embed-responsive-item" preload="none" controls loop :alt="vid.description" width="100%" height="100%" :poster="vid.preview_url">
<video slot="img" class="embed-responsive-item" preload="none" controls playsinline loop :alt="vid.description" width="100%" height="100%" :poster="vid.preview_url">
<source :src="vid.url" :type="vid.mime">
</video>
</b-carousel-slide>
@ -41,4 +41,4 @@
export default {
props: ['status']
}
</script>
</script>

View File

@ -22,7 +22,7 @@
:alt="altText(status)"/>
</div>
<div v-else class="embed-responsive embed-responsive-16by9">
<video class="video" controls preload="metadata" loop :poster="status.media_attachments[0].preview_url" :data-id="status.id">
<video class="video" controls playsinline preload="metadata" loop :poster="status.media_attachments[0].preview_url" :data-id="status.id">
<source :src="status.media_attachments[0].url" :type="status.media_attachments[0].mime">
</video>
</div>

View File

@ -1,7 +1,6 @@
<?php
return [
'about' => 'O nás',
'help' => 'Nápověda',
'language' => 'Jazyk',
@ -16,5 +15,4 @@ return [
'contact-us' => 'Kontaktujte nás',
'places' => 'Místa',
'profiles' => 'Profily',
];

View File

@ -0,0 +1,11 @@
<?php
return [
'compose' => [
'invalid' => [
'album' => 'Mindestens 1 Foto oder Video muss enthalten sein.',
],
],
];

View File

@ -1,7 +1,6 @@
<?php
return [
'search' => 'Suche',
'home' => 'Heim',
'local' => 'Lokal',
@ -16,5 +15,5 @@ return [
'admin' => 'Administration',
'logout' => 'Abmelden',
'directMessages' => 'Privatnachrichten',
'composePost' => 'Neu',
];

View File

@ -15,5 +15,5 @@ return [
'contact' => 'Kontakt',
'contact-us' => 'Kontaktiere uns',
'places' => 'Orte',
'profiles' => 'Profile',
];

View File

@ -11,6 +11,12 @@ Route::post('i/actor/inbox', 'InstanceActorController@inbox');
Route::get('i/actor/outbox', 'InstanceActorController@outbox');
Route::get('/stories/{username}/{id}', 'StoryController@getActivityObject');
Route::get('.well-known/webfinger', 'FederationController@webfinger')->name('well-known.webfinger');
Route::get('.well-known/nodeinfo', 'FederationController@nodeinfoWellKnown')->name('well-known.nodeinfo');
Route::get('.well-known/host-meta', 'FederationController@hostMeta')->name('well-known.hostMeta');
Route::redirect('.well-known/change-password', '/settings/password');
Route::get('api/nodeinfo/2.0.json', 'FederationController@nodeinfo');
Route::group(['prefix' => 'api'], function() use($middleware) {
Route::group(['prefix' => 'v1'], function() use($middleware) {

View File

@ -90,11 +90,6 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact
Auth::routes();
Route::get('.well-known/webfinger', 'FederationController@webfinger')->name('well-known.webfinger');
Route::get('.well-known/nodeinfo', 'FederationController@nodeinfoWellKnown')->name('well-known.nodeinfo');
Route::get('.well-known/host-meta', 'FederationController@hostMeta')->name('well-known.hostMeta');
Route::redirect('.well-known/change-password', '/settings/password');
Route::get('/home', 'HomeController@index')->name('home');
Route::get('discover/c/{slug}', 'DiscoverController@showCategory');
@ -105,7 +100,6 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact
Route::group(['prefix' => 'api'], function () {
Route::get('search', 'SearchController@searchAPI');
Route::get('nodeinfo/2.0.json', 'FederationController@nodeinfo');
Route::post('status/view', 'StatusController@storeView');
Route::get('v1/polls/{id}', 'PollController@getPoll');
Route::post('v1/polls/{id}/votes', 'PollController@vote');
@ -251,7 +245,6 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact
Route::post('v1/publish', 'StoryController@publishStory');
Route::delete('v1/delete/{id}', 'StoryController@apiV1Delete');
});
});
Route::get('discover/tags/{hashtag}', 'DiscoverController@showTags');