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) ## [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) ## [v0.11.1 (2021-09-07)](https://github.com/pixelfed/pixelfed/compare/v0.11.0...v0.11.1)
### Added ### Added
- WebP Support ([069a0e4a](https://github.com/pixelfed/pixelfed/commit/069a0e4a)) - 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 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 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)) - 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) ## [v0.11.0 (2021-06-01)](https://github.com/pixelfed/pixelfed/compare/v0.10.10...v0.11.0)
### Added ### Added

View file

@ -55,6 +55,7 @@ use App\Services\{
MediaPathService, MediaPathService,
PublicTimelineService, PublicTimelineService,
ProfileService, ProfileService,
RelationshipService,
SearchApiV2Service, SearchApiV2Service,
StatusService, StatusService,
MediaBlocklistService MediaBlocklistService
@ -551,7 +552,7 @@ class ApiV1Controller extends Controller
* *
* @param array|integer $id * @param array|integer $id
* *
* @return \App\Transformer\Api\RelationshipTransformer * @return \App\Services\RelationshipService
*/ */
public function accountRelationshipsById(Request $request) public function accountRelationshipsById(Request $request)
{ {
@ -563,12 +564,9 @@ class ApiV1Controller extends Controller
]); ]);
$pid = $request->user()->profile_id ?? $request->user()->profile->id; $pid = $request->user()->profile_id ?? $request->user()->profile->id;
$ids = collect($request->input('id')); $ids = collect($request->input('id'));
$filtered = $ids->filter(function($v) use($pid) { $res = $ids->map(function($id) use($pid) {
return $v != $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); return response()->json($res);
} }

View file

@ -35,14 +35,14 @@ class FederationController extends Controller
public function nodeinfoWellKnown() public function nodeinfoWellKnown()
{ {
abort_if(!config('federation.nodeinfo.enabled'), 404); 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','*'); ->header('Access-Control-Allow-Origin','*');
} }
public function nodeinfo() public function nodeinfo()
{ {
abort_if(!config('federation.nodeinfo.enabled'), 404); 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','*'); ->header('Access-Control-Allow-Origin','*');
} }
@ -53,6 +53,11 @@ class FederationController extends Controller
abort_if(!$request->filled('resource'), 400); abort_if(!$request->filled('resource'), 400);
$resource = $request->input('resource'); $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); $parsed = Nickname::normalizeProfileUrl($resource);
if(empty($parsed) || $parsed['domain'] !== config('pixelfed.domain.app')) { if(empty($parsed) || $parsed['domain'] !== config('pixelfed.domain.app')) {
abort(404); abort(404);
@ -63,8 +68,9 @@ class FederationController extends Controller
return ProfileController::accountCheck($profile); return ProfileController::accountCheck($profile);
} }
$webfinger = (new Webfinger($profile))->generate(); $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','*'); ->header('Access-Control-Allow-Origin','*');
} }

View file

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

View file

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

View file

@ -6,8 +6,11 @@ use Illuminate\Http\Request;
trait Mastodon trait Mastodon
{ {
public function mastodon() public function mastodon()
{ {
return view('settings.import.mastodon.home'); 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() public function __construct()
{ {
$this->middleware('auth'); $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 = Cache::rememberForever(InstanceActor::PROFILE_KEY, function() {
$res = (new InstanceActor())->first()->getActor(); $res = (new InstanceActor())->first()->getActor();
return json_encode($res); return json_encode($res, JSON_UNESCAPED_SLASHES);
}); });
return response($res)->header('Content-Type', 'application/json'); return response($res)->header('Content-Type', 'application/json');
} }

View file

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

View file

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

View file

@ -11,7 +11,7 @@ class InstanceActor extends Model
const PROFILE_BASE = '/i/actor'; const PROFILE_BASE = '/i/actor';
const KEY_ID = '/i/actor#main-key'; 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_PUBLIC = 'federation:_v1:instance:actor:profile:pki_public';
const PKI_PRIVATE = 'federation:_v1:instance:actor:profile:pki_private'; 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\{ use App\Observers\{
AvatarObserver, AvatarObserver,
FollowerObserver,
LikeObserver, LikeObserver,
NotificationObserver, NotificationObserver,
ModLogObserver, ModLogObserver,
@ -15,7 +14,6 @@ use App\Observers\{
}; };
use App\{ use App\{
Avatar, Avatar,
Follower,
Like, Like,
Notification, Notification,
ModLog, ModLog,
@ -50,7 +48,6 @@ class AppServiceProvider extends ServiceProvider
StatusHashtag::observe(StatusHashtagObserver::class); StatusHashtag::observe(StatusHashtagObserver::class);
User::observe(UserObserver::class); User::observe(UserObserver::class);
UserFilter::observe(UserFilterObserver::class); UserFilter::observe(UserFilterObserver::class);
Follower::observe(FollowerObserver::class);
Horizon::auth(function ($request) { Horizon::auth(function ($request) {
return Auth::check() && $request->user()->is_admin; return Auth::check() && $request->user()->is_admin;
}); });

View file

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

View file

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

View file

@ -7,6 +7,13 @@ use App\Instance;
class InstanceService 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() public static function getBannedDomains()
{ {
return Cache::remember('instances:banned:domains', now()->addHours(12), function() { return Cache::remember('instances:banned:domains', now()->addHours(12), function() {

View file

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

View file

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

View file

@ -2,50 +2,51 @@
namespace App\Transformer\Api; namespace App\Transformer\Api;
use App\{ use App\Notification;
Notification, use App\Services\AccountService;
Status
};
use App\Services\HashidService; use App\Services\HashidService;
use App\Services\RelationshipService;
use App\Services\StatusService;
use League\Fractal; use League\Fractal;
class NotificationTransformer extends Fractal\TransformerAbstract class NotificationTransformer extends Fractal\TransformerAbstract
{ {
protected $defaultIncludes = [
'account',
'status',
'relationship',
'modlog',
'tagged'
];
public function transform(Notification $notification) public function transform(Notification $notification)
{ {
return [ $res = [
'id' => (string) $notification->id, 'id' => (string) $notification->id,
'type' => $this->replaceTypeVerb($notification->action), 'type' => $this->replaceTypeVerb($notification->action),
'created_at' => (string) $notification->created_at->format('c'), 'created_at' => (string) $notification->created_at->format('c'),
]; ];
}
public function includeAccount(Notification $notification) $n = $notification;
{
return $this->item($notification->actor, new AccountTransformer());
}
public function includeStatus(Notification $notification) if($n->actor_id) {
{ $res['account'] = AccountService::get($n->actor_id);
$item = $notification; $res['relationship'] = RelationshipService::get($n->actor_id, $n->profile_id);
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->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) public function replaceTypeVerb($verb)
@ -57,56 +58,21 @@ class NotificationTransformer extends Fractal\TransformerAbstract
'reblog' => 'share', 'reblog' => 'share',
'share' => 'share', 'share' => 'share',
'like' => 'favourite', 'like' => 'favourite',
'group:like' => 'favourite',
'comment' => 'comment', 'comment' => 'comment',
'admin.user.modlog.comment' => 'modlog', 'admin.user.modlog.comment' => 'modlog',
'tagged' => 'tagged', 'tagged' => 'tagged',
'group:comment' => 'group:comment', 'group:comment' => 'group:comment',
'story:react' => 'story:react', '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]; 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); $digest = self::_digest($body);
} }
$headers = self::_headersToSign($url, $body ? $digest : false); $headers = self::_headersToSign($url, $body ? $digest : false);
$headers = array_unique(array_merge($headers, $addlHeaders)); $headers = array_merge($headers, $addlHeaders);
$stringToSign = self::_headersToSigningString($headers); $stringToSign = self::_headersToSigningString($headers);
$signedHeaders = implode(' ', array_map('strtolower', array_keys($headers))); $signedHeaders = implode(' ', array_map('strtolower', array_keys($headers)));
$key = openssl_pkey_get_private($privateKey); $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'), 'Date' => $date->format('D, d M Y H:i:s \G\M\T'),
'Host' => parse_url($url, PHP_URL_HOST), 'Host' => parse_url($url, PHP_URL_HOST),
'Accept' => 'application/activity+json, application/json', 'Accept' => 'application/activity+json, application/json',
'Content-Type' => 'application/activity+json'
]; ];
if($digest) { if($digest) {

View file

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

View file

@ -10,34 +10,29 @@ class Nodeinfo {
public static function get() public static function get()
{ {
$res = Cache::remember('api:nodeinfo', now()->addMinutes(15), function () { $res = Cache::remember('api:nodeinfo', 300, function () {
$activeHalfYear = Cache::remember('api:nodeinfo:ahy', now()->addHours(12), function() { $activeHalfYear = Cache::remember('api:nodeinfo:ahy', 172800, function() {
// todo: replace with last_active_at after July 9, 2021 (96afc3e781) return User::select('last_active_at')
$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')
->where('last_active_at', '>', now()->subMonths(6)) ->where('last_active_at', '>', now()->subMonths(6))
->pluck('profile_id') ->orWhere('created_at', '>', now()->subMonths(6))
->toArray(); ->count();
$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();
}); });
$activeMonth = Cache::remember('api:nodeinfo:am', now()->addHours(2), function() {
$activeMonth = Cache::remember('api:nodeinfo:am', 172800, function() {
return User::select('last_active_at') return User::select('last_active_at')
->where('last_active_at', '>', now()->subMonths(1)) ->where('last_active_at', '>', now()->subMonths(1))
->orWhere('created_at', '>', now()->subMonths(1)) ->orWhere('created_at', '>', now()->subMonths(1))
->count(); ->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 [ return [
'metadata' => [ 'metadata' => [
'nodeName' => config_cache('app.name'), 'nodeName' => config_cache('app.name'),
@ -59,10 +54,10 @@ class Nodeinfo {
'version' => config('pixelfed.version'), 'version' => config('pixelfed.version'),
], ],
'usage' => [ 'usage' => [
'localPosts' => Status::whereLocal(true)->count(), 'localPosts' => $statuses,
'localComments' => 0, 'localComments' => 0,
'users' => [ 'users' => [
'total' => User::count(), 'total' => $users,
'activeHalfyear' => (int) $activeHalfYear, 'activeHalfyear' => (int) $activeHalfYear,
'activeMonth' => (int) $activeMonth, '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', 'provider' => 'users',
'table' => 'password_resets', 'table' => 'password_resets',
'expire' => 60, '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"; return Math.floor(seconds) + "s";
}), }),
timeAhead: (function(ts) { timeAhead: (function(ts, short = true) {
let date = Date.parse(ts); let date = Date.parse(ts);
let diff = date - Date.parse(new Date()); let diff = date - Date.parse(new Date());
let seconds = Math.floor((diff) / 1000); let seconds = Math.floor((diff) / 1000);
let interval = Math.floor(seconds / 63072000); let interval = Math.floor(seconds / 63072000);
if (interval >= 1) { if (interval >= 1) {
return interval + "y"; return interval + (short ? "y" : " years");
} }
interval = Math.floor(seconds / 604800); interval = Math.floor(seconds / 604800);
if (interval >= 1) { if (interval >= 1) {
return interval + "w"; return interval + (short ? "w" : " weeks");
} }
interval = Math.floor(seconds / 86400); interval = Math.floor(seconds / 86400);
if (interval >= 1) { if (interval >= 1) {
return interval + "d"; return interval + (short ? "d" : " days");
} }
interval = Math.floor(seconds / 3600); interval = Math.floor(seconds / 3600);
if (interval >= 1) { if (interval >= 1) {
return interval + "h"; return interval + (short ? "h" : " hours");
} }
interval = Math.floor(seconds / 60); interval = Math.floor(seconds / 60);
if (interval >= 1) { 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) { rewriteLinks: (function(i) {
@ -234,7 +234,8 @@ window.App.util = {
'filter-willow': 'brightness(1.2) contrast(.85) saturate(.05) sepia(.2)', '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)' 'filter-xpro-ii': 'sepia(.45) contrast(1.25) brightness(1.75) saturate(1.3) hue-rotate(-5deg)'
}, },
emoji: ['😂','💯','❤️','🙌','👏','👌','😍','😯','😢','😅','😁','🙂','😎','😀','🤣','😃','😄','😆','😉','😊','😋','😘','😗','😙','😚','🤗','🤩','🤔','🤨','😐','😑','😶','🙄','😏','😣','😥','😮','🤐','😪','😫','😴','😌','😛','😜','😝','🤤','😒','😓','😔','😕','🙃','🤑','😲','🙁','😖','😞','😟','😤','😭','😦','😧','😨','😩','🤯','😬','😰','😱','😳','🤪','😵','😡','😠','🤬','😷','🤒','🤕','🤢','🤮','🤧','😇','🤠','🤡','🤥','🤫','🤭','🧐','🤓','😈','👿','👹','👺','💀','👻','👽','🤖','💩','😺','😸','😹','😻','😼','😽','🙀','😿','😾','🤲','👐','🤝','👍','👎','👊','✊','🤛','🤜','🤞','✌️','🤟','🤘','👈','👉','👆','👇','☝️','✋','🤚','🖐','🖖','👋','🤙','💪','🖕','✍️','🙏','💍','💄','💋','👄','👅','👂','👃','👣','👁','👀','🧠','🗣','👤','👥' emoji: [
'😂','💯','❤️','🙌','👏','👌','😍','😯','😢','😅','😁','🙂','😎','😀','🤣','😃','😄','😆','😉','😊','😋','😘','😗','😙','😚','🤗','🤩','🤔','🤨','😐','😑','😶','🙄','😏','😣','😥','😮','🤐','😪','😫','😴','😌','😛','😜','😝','🤤','😒','😓','😔','😕','🙃','🤑','😲','🙁','😖','😞','😟','😤','😭','😦','😧','😨','😩','🤯','😬','😰','😱','😳','🤪','😵','😡','😠','🤬','😷','🤒','🤕','🤢','🤮','🤧','😇','🤠','🤡','🤥','🤫','🤭','🧐','🤓','😈','👿','👹','👺','💀','👻','👽','🤖','💩','😺','😸','😹','😻','😼','😽','🙀','😿','😾','🤲','👐','🤝','👍','👎','👊','✊','🤛','🤜','🤞','✌️','🤟','🤘','👈','👉','👆','👇','☝️','✋','🤚','🖐','🖖','👋','🤙','💪','🖕','✍️','🙏','💍','💄','💋','👄','👅','👂','👃','👣','👁','👀','🧠','🗣','👤','👥'
], ],
embed: { embed: {
post: (function(url, caption = true, likes = false, layout = 'full') { 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>. <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> </p>
</div> </div>
<div v-else-if="n.type == 'comment'"> <div v-else-if="n.type == 'comment'">
<p class="my-0"> <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>. <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> </p>
</div> </div>
<div v-else-if="n.type == 'group:comment'"> <div v-else-if="n.type == 'group:comment'">
<p class="my-0"> <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>. <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> </p>
</div> </div>
<div v-else-if="n.type == 'story:react'"> <div v-else-if="n.type == 'story:react'">
<p class="my-0"> <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>. <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> </p>
</div> </div>
<div v-else-if="n.type == 'story:comment'"> <div v-else-if="n.type == 'story:comment'">
<p class="my-0"> <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>. <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> </p>
</div> </div>
<div v-else-if="n.type == 'mention'"> <div v-else-if="n.type == 'mention'">
<p class="my-0"> <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. <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> </p>
</div> </div>
<div v-else-if="n.type == 'follow'"> <div v-else-if="n.type == 'follow'">
<p class="my-0"> <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. <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> </p>
</div> </div>
<div v-else-if="n.type == 'share'"> <div v-else-if="n.type == 'share'">
<p class="my-0"> <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>. <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> </p>
</div> </div>
<div v-else-if="n.type == 'modlog'"> <div v-else-if="n.type == 'modlog'">
<p class="my-0"> <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>. <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> </p>
</div> </div>
<div v-else-if="n.type == 'tagged'"> <div v-else-if="n.type == 'tagged'">
<p class="my-0"> <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>. <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> </p>
</div> </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"> <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> <span class="small text-muted" data-toggle="tooltip" data-placement="bottom" :title="n.created_at">{{timeAgo(n.created_at)}}</span>
</div> </div>
@ -105,7 +109,7 @@
</a> </a>
</div> --> </div> -->
<div v-else> <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> </div>
</div> </div>
@ -209,6 +213,9 @@ export default {
} }
return true; return true;
}); });
let ids = data.map(n => n.id);
this.notificationMaxId = Math.max(...ids);
this.notifications.push(...data); this.notifications.push(...data);
this.notificationCursor++; this.notificationCursor++;
$state.loaded(); $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 d-md-none align-items-center justify-content-between card-header bg-white w-100">
<div class="d-flex"> <div class="d-flex">
<div class="status-avatar mr-2" @click="redirect(profileUrl)"> <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>
<div class="username"> <div class="username">
<span class="username-link font-weight-bold text-dark cursor-pointer" @click="redirect(profileUrl)">{{ statusUsername }}</span> <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-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="d-flex align-items-center status-username text-truncate">
<div class="status-avatar mr-2" @click="redirect(profileUrl)"> <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>
<div class="username"> <div class="username">
<span class="username-link font-weight-bold text-dark cursor-pointer" @click="redirect(profileUrl)">{{ statusUsername }}</span> <span class="username-link font-weight-bold text-dark cursor-pointer" @click="redirect(profileUrl)">{{ statusUsername }}</span>
@ -157,7 +157,7 @@
</p> </p>
<div class="comments mt-3"> <div class="comments mt-3">
<div v-for="(reply, index) in results" class="pb-4 media" :key="'tl' + reply.id + '_' + index"> <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 class="media-body">
<div v-if="reply.sensitive == true"> <div v-if="reply.sensitive == true">
<span class="py-3"> <span class="py-3">
@ -190,7 +190,7 @@
</div> </div>
<div v-if="reply.thread == true" class="comment-thread"> <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"> <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"> <div class="media-body">
<p class="d-flex justify-content-between align-items-top read-more" style="overflow-y: hidden;"> <p class="d-flex justify-content-between align-items-top read-more" style="overflow-y: hidden;">
<span> <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="list-group-item border-0 py-1" v-for="(user, index) in likes" :key="'modal_likes_'+index">
<div class="media"> <div class="media">
<a :href="user.url"> <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> </a>
<div class="media-body"> <div class="media-body">
<p class="mb-0" style="font-size: 14px"> <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="list-group-item border-0 py-1" v-for="(user, index) in shares" :key="'modal_shares_'+index">
<div class="media"> <div class="media">
<a :href="user.url"> <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> </a>
<div class="media-body"> <div class="media-body">
<div class="d-inline-block"> <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="list-group-item border-0 py-1" v-for="(taguser, index) in status.taggedPeople" :key="'modal_taggedpeople_'+index">
<div class="media"> <div class="media">
<a :href="'/'+taguser.username"> <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> </a>
<div class="media-body"> <div class="media-body">
<p class="pt-1 d-flex justify-content-between" style="font-size: 14px"> <p class="pt-1 d-flex justify-content-between" style="font-size: 14px">

View file

@ -19,7 +19,7 @@
</div> </div>
<div class="card-body pb-0"> <div class="card-body pb-0">
<div class="mt-n5 mb-3"> <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 class="float-right mt-n1">
<span> <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> <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;"> <div style="margin-top:-2px;">
<story-component v-if="config.features.stories" :scope="scope"></story-component> <story-component v-if="config.features.stories" :scope="scope"></story-component>
</div> </div>
<div> <div class="pt-4">
<div v-if="loading" class="text-center" style="padding-top:10px;"> <div v-if="loading" class="text-center" style="padding-top:10px;">
<div class="spinner-border" role="status"> <div class="spinner-border" role="status">
<span class="sr-only">Loading...</span> <span class="sr-only">Loading...</span>
@ -106,6 +106,8 @@
size="small" size="small"
v-on:status-delete="deleteStatus" v-on:status-delete="deleteStatus"
v-on:comment-focus="commentFocus" v-on:comment-focus="commentFocus"
v-on:followed="followedAccount"
v-on:unfollowed="unfollowedAccount"
/> />
</div> </div>
@ -1067,7 +1069,29 @@
this.feed = this.feed.filter(s => { this.feed = this.feed.filter(s => {
return s.id != status; 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 () { 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> <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> </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;"> <div class="text-right" style="flex-grow:1;">
<button class="btn btn-link text-dark py-0" type="button" @click="ctxMenu()"> <button class="btn btn-link text-dark py-0" type="button" @click="ctxMenu()">
<span class="fas fa-ellipsis-h text-lighter"></span> <span class="fas fa-ellipsis-h text-lighter"></span>
@ -382,6 +390,52 @@
statusDeleted(status) { statusDeleted(status) {
this.$emit('status-delete', 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'"> <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"> <source :src="media.url" :type="media.mime">
</video> </video>
@ -72,4 +72,4 @@
export default { export default {
props: ['status'] props: ['status']
} }
</script> </script>

View file

@ -13,7 +13,7 @@
:interval="0" :interval="0"
> >
<b-carousel-slide v-for="(vid, index) in status.media_attachments" :key="vid.id + '-media'"> <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"> <source :src="vid.url" :type="vid.mime">
</video> </video>
</b-carousel-slide> </b-carousel-slide>
@ -29,7 +29,7 @@
:interval="0" :interval="0"
> >
<b-carousel-slide v-for="(vid, index) in status.media_attachments" :key="vid.id + '-media'"> <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"> <source :src="vid.url" :type="vid.mime">
</video> </video>
</b-carousel-slide> </b-carousel-slide>
@ -41,4 +41,4 @@
export default { export default {
props: ['status'] props: ['status']
} }
</script> </script>

View file

@ -22,7 +22,7 @@
:alt="altText(status)"/> :alt="altText(status)"/>
</div> </div>
<div v-else class="embed-responsive embed-responsive-16by9"> <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"> <source :src="status.media_attachments[0].url" :type="status.media_attachments[0].mime">
</video> </video>
</div> </div>

View file

@ -1,7 +1,6 @@
<?php <?php
return [ return [
'about' => 'O nás', 'about' => 'O nás',
'help' => 'Nápověda', 'help' => 'Nápověda',
'language' => 'Jazyk', 'language' => 'Jazyk',
@ -16,5 +15,4 @@ return [
'contact-us' => 'Kontaktujte nás', 'contact-us' => 'Kontaktujte nás',
'places' => 'Místa', 'places' => 'Místa',
'profiles' => 'Profily', '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 <?php
return [ return [
'search' => 'Suche', 'search' => 'Suche',
'home' => 'Heim', 'home' => 'Heim',
'local' => 'Lokal', 'local' => 'Lokal',
@ -16,5 +15,5 @@ return [
'admin' => 'Administration', 'admin' => 'Administration',
'logout' => 'Abmelden', 'logout' => 'Abmelden',
'directMessages' => 'Privatnachrichten', 'directMessages' => 'Privatnachrichten',
'composePost' => 'Neu',
]; ];

View file

@ -15,5 +15,5 @@ return [
'contact' => 'Kontakt', 'contact' => 'Kontakt',
'contact-us' => 'Kontaktiere uns', 'contact-us' => 'Kontaktiere uns',
'places' => 'Orte', '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('i/actor/outbox', 'InstanceActorController@outbox');
Route::get('/stories/{username}/{id}', 'StoryController@getActivityObject'); 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' => 'api'], function() use($middleware) {
Route::group(['prefix' => 'v1'], 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(); 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('/home', 'HomeController@index')->name('home');
Route::get('discover/c/{slug}', 'DiscoverController@showCategory'); 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::group(['prefix' => 'api'], function () {
Route::get('search', 'SearchController@searchAPI'); Route::get('search', 'SearchController@searchAPI');
Route::get('nodeinfo/2.0.json', 'FederationController@nodeinfo');
Route::post('status/view', 'StatusController@storeView'); Route::post('status/view', 'StatusController@storeView');
Route::get('v1/polls/{id}', 'PollController@getPoll'); Route::get('v1/polls/{id}', 'PollController@getPoll');
Route::post('v1/polls/{id}/votes', 'PollController@vote'); 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::post('v1/publish', 'StoryController@publishStory');
Route::delete('v1/delete/{id}', 'StoryController@apiV1Delete'); Route::delete('v1/delete/{id}', 'StoryController@apiV1Delete');
}); });
}); });
Route::get('discover/tags/{hashtag}', 'DiscoverController@showTags'); Route::get('discover/tags/{hashtag}', 'DiscoverController@showTags');