mirror of
https://github.com/pixelfed/pixelfed.git
synced 2024-12-23 13:33:18 +00:00
commit
853d62e83f
44 changed files with 1167 additions and 787 deletions
13
CHANGELOG.md
13
CHANGELOG.md
|
@ -4,7 +4,18 @@
|
|||
|
||||
### Updated
|
||||
- Updated NotificationService, fix 500 bug. ([4a609dc3](https://github.com/pixelfed/pixelfed/commit/4a609dc3))
|
||||
- Update HttpSignatures, update instance actor headers. Fixes #2935. ([a900de21](https://github.com/pixelfed/pixelfed/commit/a900de21))
|
||||
- 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)
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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','*');
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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() {
|
||||
|
|
86
app/Services/RelationshipService.php
Normal file
86
app/Services/RelationshipService.php
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -35,7 +35,7 @@ class Note extends Fractal\TransformerAbstract
|
|||
'href' => $parent->permalink(),
|
||||
'name' => $name
|
||||
];
|
||||
$mentions = array_merge($reply, $mentions);
|
||||
array_push($mentions, $reply);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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':
|
||||
|
|
|
@ -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
1261
composer.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -96,6 +96,7 @@ return [
|
|||
'provider' => 'users',
|
||||
'table' => 'password_resets',
|
||||
'expire' => 60,
|
||||
'throttle' => 60,
|
||||
],
|
||||
],
|
||||
|
||||
|
|
BIN
public/js/activity.js
vendored
BIN
public/js/activity.js
vendored
Binary file not shown.
BIN
public/js/admin.js
vendored
BIN
public/js/admin.js
vendored
Binary file not shown.
BIN
public/js/app.js
vendored
BIN
public/js/app.js
vendored
Binary file not shown.
BIN
public/js/components.js
vendored
BIN
public/js/components.js
vendored
Binary file not shown.
BIN
public/js/direct.js
vendored
BIN
public/js/direct.js
vendored
Binary file not shown.
BIN
public/js/memoryprofile.js
vendored
BIN
public/js/memoryprofile.js
vendored
Binary file not shown.
BIN
public/js/profile.js
vendored
BIN
public/js/profile.js
vendored
Binary file not shown.
BIN
public/js/rempos.js
vendored
BIN
public/js/rempos.js
vendored
Binary file not shown.
BIN
public/js/rempro.js
vendored
BIN
public/js/rempro.js
vendored
Binary file not shown.
BIN
public/js/status.js
vendored
BIN
public/js/status.js
vendored
Binary file not shown.
BIN
public/js/theme-monokai.js
vendored
BIN
public/js/theme-monokai.js
vendored
Binary file not shown.
BIN
public/js/timeline.js
vendored
BIN
public/js/timeline.js
vendored
Binary file not shown.
BIN
public/js/vendor.js
vendored
BIN
public/js/vendor.js
vendored
Binary file not shown.
Binary file not shown.
17
resources/assets/js/app.js
vendored
17
resources/assets/js/app.js
vendored
|
@ -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') {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 () {
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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');
|
||||
|
|
Loading…
Reference in a new issue