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 1227 additions and 823 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,
],
],

BIN
public/js/activity.js vendored

Binary file not shown.

BIN
public/js/admin.js vendored

Binary file not shown.

BIN
public/js/app.js vendored

Binary file not shown.

Binary file not shown.

BIN
public/js/direct.js vendored

Binary file not shown.

Binary file not shown.

BIN
public/js/profile.js vendored

Binary file not shown.

BIN
public/js/rempos.js vendored

Binary file not shown.

BIN
public/js/rempro.js vendored

Binary file not shown.

BIN
public/js/status.js vendored

Binary file not shown.

Binary file not shown.

BIN
public/js/timeline.js vendored

Binary file not shown.

BIN
public/js/vendor.js vendored

Binary file not shown.

Binary file not shown.

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');