mirror of
https://github.com/pixelfed/pixelfed.git
synced 2025-01-10 14:10:46 +00:00
commit
7353bc2d8c
37 changed files with 694 additions and 108 deletions
10
CHANGELOG.md
10
CHANGELOG.md
|
@ -10,6 +10,9 @@
|
|||
- Add Instagram Import ([e2a6bdd0](https://github.com/pixelfed/pixelfed/commit/e2a6bdd0))
|
||||
- Add notification preview to NotificationCard ([28445e27](https://github.com/pixelfed/pixelfed/commit/28445e27))
|
||||
- Add Grid Mode to Timelines ([c1853ca8](https://github.com/pixelfed/pixelfed/commit/c1853ca8))
|
||||
- Add MediaPathService ([c54b29c5](https://github.com/pixelfed/pixelfed/commit/c54b29c5))
|
||||
- Add Media Tags ([711fc020](https://github.com/pixelfed/pixelfed/commit/711fc020))
|
||||
- Add MediaTagService ([524c6d45](https://github.com/pixelfed/pixelfed/commit/524c6d45))
|
||||
|
||||
### Updated
|
||||
- Updated PostComponent, fix remote urls ([42716ccc](https://github.com/pixelfed/pixelfed/commit/42716ccc))
|
||||
|
@ -59,6 +62,13 @@
|
|||
- Updated Timeline.vue, hide like counts on grid mode. Fixes ([#2293](https://github.com/pixelfed/pixelfed/issues/2293)) ([cc18159f](https://github.com/pixelfed/pixelfed/commit/cc18159f))
|
||||
- Updated Timeline.vue, make grid mode photos clickable. Fixes ([#2292](https://github.com/pixelfed/pixelfed/issues/2292)) ([6db68184](https://github.com/pixelfed/pixelfed/commit/6db68184))
|
||||
- Updated ComposeModal.vue, use vue tooltips. Fixes ([#2142](https://github.com/pixelfed/pixelfed/issues/2142)) ([2b753123](https://github.com/pixelfed/pixelfed/commit/2b753123))
|
||||
- Updated AccountController, prevent blocking admins. ([2c440b48](https://github.com/pixelfed/pixelfed/commit/2c440b48))
|
||||
- Updated Api controllers to use MediaPathService. ([58864212](https://github.com/pixelfed/pixelfed/commit/58864212))
|
||||
- Updated notification components, add modlog and tagged notification types ([51862b8b](https://github.com/pixelfed/pixelfed/commit/51862b8b))
|
||||
- Updated StoryController, allow video stories. ([b3b220b9](https://github.com/pixelfed/pixelfed/commit/b3b220b9))
|
||||
- Updated InternalApiController, add media tags. ([ee93f459](https://github.com/pixelfed/pixelfed/commit/ee93f459))
|
||||
- Updated ComposeModal.vue, add media tagging. ([421ea022](https://github.com/pixelfed/pixelfed/commit/421ea022))
|
||||
- Updated NotificationTransformer, add modlog and tagged types. ([49dab6fb](https://github.com/pixelfed/pixelfed/commit/49dab6fb))
|
||||
|
||||
## [v0.10.9 (2020-04-17)](https://github.com/pixelfed/pixelfed/compare/v0.10.8...v0.10.9)
|
||||
### Added
|
||||
|
|
|
@ -244,7 +244,7 @@ class AccountController extends Controller
|
|||
switch ($type) {
|
||||
case 'user':
|
||||
$profile = Profile::findOrFail($item);
|
||||
if ($profile->id == $user->id) {
|
||||
if ($profile->id == $user->id || $profile->user->is_admin == true) {
|
||||
return abort(403);
|
||||
}
|
||||
$class = get_class($profile);
|
||||
|
|
|
@ -47,6 +47,7 @@ use App\Jobs\VideoPipeline\{
|
|||
};
|
||||
use App\Services\{
|
||||
NotificationService,
|
||||
MediaPathService,
|
||||
SearchApiV2Service
|
||||
};
|
||||
|
||||
|
@ -646,6 +647,10 @@ class ApiV1Controller extends Controller
|
|||
|
||||
$profile = Profile::findOrFail($id);
|
||||
|
||||
if($profile->user->is_admin == true) {
|
||||
abort(400, 'You cannot block an admin');
|
||||
}
|
||||
|
||||
Follower::whereProfileId($profile->id)->whereFollowingId($pid)->delete();
|
||||
Follower::whereProfileId($pid)->whereFollowingId($profile->id)->delete();
|
||||
Notification::whereProfileId($pid)->whereActorId($profile->id)->delete();
|
||||
|
@ -1030,9 +1035,6 @@ class ApiV1Controller extends Controller
|
|||
$filterClass = in_array($request->input('filter_class'), Filter::classes()) ? $request->input('filter_class') : null;
|
||||
$filterName = in_array($request->input('filter_name'), Filter::names()) ? $request->input('filter_name') : null;
|
||||
|
||||
$monthHash = hash('sha1', date('Y').date('m'));
|
||||
$userHash = hash('sha1', $user->id . (string) $user->created_at);
|
||||
|
||||
$photo = $request->file('file');
|
||||
|
||||
$mimes = explode(',', config('pixelfed.media_types'));
|
||||
|
@ -1040,7 +1042,7 @@ class ApiV1Controller extends Controller
|
|||
abort(403, 'Invalid or unsupported mime type.');
|
||||
}
|
||||
|
||||
$storagePath = "public/m/{$monthHash}/{$userHash}";
|
||||
$storagePath = MediaPathService::get($user, 2);
|
||||
$path = $photo->store($storagePath);
|
||||
$hash = \hash_file('sha256', $photo);
|
||||
|
||||
|
@ -1916,7 +1918,7 @@ class ApiV1Controller extends Controller
|
|||
foreach($bookmarks as $id) {
|
||||
$res[] = \App\Services\StatusService::get($id);
|
||||
}
|
||||
return response()->json($res, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
|
||||
return $res;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -35,6 +35,7 @@ use App\Jobs\VideoPipeline\{
|
|||
VideoThumbnail
|
||||
};
|
||||
use App\Services\NotificationService;
|
||||
use App\Services\MediaPathService;
|
||||
|
||||
class BaseApiController extends Controller
|
||||
{
|
||||
|
@ -235,9 +236,6 @@ class BaseApiController extends Controller
|
|||
$filterClass = in_array($request->input('filter_class'), Filter::classes()) ? $request->input('filter_class') : null;
|
||||
$filterName = in_array($request->input('filter_name'), Filter::names()) ? $request->input('filter_name') : null;
|
||||
|
||||
$monthHash = hash('sha1', date('Y').date('m'));
|
||||
$userHash = hash('sha1', $user->id . (string) $user->created_at);
|
||||
|
||||
$photo = $request->file('file');
|
||||
|
||||
$mimes = explode(',', config('pixelfed.media_types'));
|
||||
|
@ -245,7 +243,7 @@ class BaseApiController extends Controller
|
|||
return;
|
||||
}
|
||||
|
||||
$storagePath = "public/m/{$monthHash}/{$userHash}";
|
||||
$storagePath = MediaPathService::get($user, 2);
|
||||
$path = $photo->store($storagePath);
|
||||
$hash = \hash_file('sha256', $photo);
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ use App\{
|
|||
Follower,
|
||||
Like,
|
||||
Media,
|
||||
MediaTag,
|
||||
Notification,
|
||||
Profile,
|
||||
StatusHashtag,
|
||||
|
@ -30,6 +31,7 @@ use League\Fractal\Serializer\ArraySerializer;
|
|||
use League\Fractal\Pagination\IlluminatePaginatorAdapter;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Illuminate\Support\Str;
|
||||
use App\Services\MediaTagService;
|
||||
use App\Services\ModLogService;
|
||||
use App\Services\PublicTimelineService;
|
||||
|
||||
|
@ -258,7 +260,8 @@ class InternalApiController extends Controller
|
|||
'cw' => 'nullable|boolean',
|
||||
'visibility' => 'required|string|in:public,private,unlisted|min:2|max:10',
|
||||
'place' => 'nullable',
|
||||
'comments_disabled' => 'nullable'
|
||||
'comments_disabled' => 'nullable',
|
||||
'tagged' => 'nullable'
|
||||
]);
|
||||
|
||||
if(config('costar.enabled') == true) {
|
||||
|
@ -282,6 +285,7 @@ class InternalApiController extends Controller
|
|||
$mimes = [];
|
||||
$place = $request->input('place');
|
||||
$cw = $request->input('cw');
|
||||
$tagged = $request->input('tagged');
|
||||
|
||||
foreach($medias as $k => $media) {
|
||||
if($k + 1 > config('pixelfed.max_album_length')) {
|
||||
|
@ -328,6 +332,21 @@ class InternalApiController extends Controller
|
|||
$media->save();
|
||||
}
|
||||
|
||||
foreach($tagged as $tg) {
|
||||
$mt = new MediaTag;
|
||||
$mt->status_id = $status->id;
|
||||
$mt->media_id = $status->media->first()->id;
|
||||
$mt->profile_id = $tg['id'];
|
||||
$mt->tagged_username = $tg['name'];
|
||||
$mt->is_public = true; // (bool) $tg['privacy'] ?? 1;
|
||||
$mt->metadata = json_encode([
|
||||
'_v' => 1,
|
||||
]);
|
||||
$mt->save();
|
||||
MediaTagService::set($mt->status_id, $mt->profile_id);
|
||||
MediaTagService::sendNotification($mt);
|
||||
}
|
||||
|
||||
$visibility = $profile->unlisted == true && $visibility == 'public' ? 'unlisted' : $visibility;
|
||||
$cw = $profile->cw == true ? true : $cw;
|
||||
$status->is_nsfw = $cw;
|
||||
|
|
55
app/Http/Controllers/MediaTagController.php
Normal file
55
app/Http/Controllers/MediaTagController.php
Normal file
|
@ -0,0 +1,55 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use App\MediaTag;
|
||||
use App\Profile;
|
||||
use App\UserFilter;
|
||||
use App\User;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class MediaTagController extends Controller
|
||||
{
|
||||
public function usernameLookup(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 403);
|
||||
|
||||
$this->validate($request, [
|
||||
'q' => 'required|string|min:1|max:50'
|
||||
]);
|
||||
|
||||
$q = $request->input('q');
|
||||
|
||||
if(Str::of($q)->startsWith('@')) {
|
||||
if(strlen($q) < 3) {
|
||||
return [];
|
||||
}
|
||||
$q = mb_substr($q, 1);
|
||||
}
|
||||
|
||||
$blocked = UserFilter::whereFilterableType('App\Profile')
|
||||
->whereFilterType('block')
|
||||
->whereFilterableId($request->user()->profile_id)
|
||||
->pluck('user_id');
|
||||
|
||||
$blocked->push($request->user()->profile_id);
|
||||
|
||||
$results = Profile::select('id','domain','username')
|
||||
->whereNotIn('id', $blocked)
|
||||
->whereNull('domain')
|
||||
->where('username','like','%'.$q.'%')
|
||||
->limit(15)
|
||||
->get()
|
||||
->map(function($r) {
|
||||
return [
|
||||
'id' => (string) $r->id,
|
||||
'name' => $r->username,
|
||||
'privacy' => true,
|
||||
'avatar' => $r->avatarUrl()
|
||||
];
|
||||
});
|
||||
|
||||
return $results;
|
||||
}
|
||||
}
|
|
@ -17,6 +17,7 @@ use Illuminate\Http\Request;
|
|||
use League\Fractal;
|
||||
use App\Util\Media\Filter;
|
||||
use Illuminate\Support\Str;
|
||||
use App\Services\HashidService;
|
||||
|
||||
class StatusController extends Controller
|
||||
{
|
||||
|
@ -65,6 +66,16 @@ class StatusController extends Controller
|
|||
return view($template, compact('user', 'status'));
|
||||
}
|
||||
|
||||
public function shortcodeRedirect(Request $request, $id)
|
||||
{
|
||||
if(strlen($id) < 5 || !Auth::check()) {
|
||||
return redirect('/login?next='.urlencode('/' . $request->path()));
|
||||
}
|
||||
$id = HashidService::decode($id);
|
||||
$status = Status::findOrFail($id);
|
||||
return redirect($status->url());
|
||||
}
|
||||
|
||||
public function showId(int $id)
|
||||
{
|
||||
abort(404);
|
||||
|
|
|
@ -24,7 +24,7 @@ class StoryController extends Controller
|
|||
'file' => function() {
|
||||
return [
|
||||
'required',
|
||||
'mimes:image/jpeg,image/png',
|
||||
'mimes:image/jpeg,image/png,video/mp4',
|
||||
'max:' . config('pixelfed.max_photo_size'),
|
||||
];
|
||||
},
|
||||
|
@ -42,7 +42,7 @@ class StoryController extends Controller
|
|||
$story = new Story();
|
||||
$story->duration = 3;
|
||||
$story->profile_id = $user->profile_id;
|
||||
$story->type = 'photo';
|
||||
$story->type = Str::endsWith($photo->getMimeType(), 'mp4') ? 'video' :'photo';
|
||||
$story->mime = $photo->getMimeType();
|
||||
$story->path = $path;
|
||||
$story->local = true;
|
||||
|
@ -65,7 +65,8 @@ class StoryController extends Controller
|
|||
$mimes = explode(',', config('pixelfed.media_types'));
|
||||
if(in_array($photo->getMimeType(), [
|
||||
'image/jpeg',
|
||||
'image/png'
|
||||
'image/png',
|
||||
'video/mp4'
|
||||
]) == false) {
|
||||
abort(400, 'Invalid media type');
|
||||
return;
|
||||
|
@ -73,11 +74,13 @@ class StoryController extends Controller
|
|||
|
||||
$storagePath = "public/_esm.t2/{$monthHash}/{$sid}/{$rid}";
|
||||
$path = $photo->store($storagePath);
|
||||
$fpath = storage_path('app/' . $path);
|
||||
$img = Intervention::make($fpath);
|
||||
$img->orientate();
|
||||
$img->save($fpath, config('pixelfed.image_quality'));
|
||||
$img->destroy();
|
||||
if(in_array($photo->getMimeType(), ['image/jpeg','image/png',])) {
|
||||
$fpath = storage_path('app/' . $path);
|
||||
$img = Intervention::make($fpath);
|
||||
$img->orientate();
|
||||
$img->save($fpath, config('pixelfed.image_quality'));
|
||||
$img->destroy();
|
||||
}
|
||||
return $path;
|
||||
}
|
||||
|
||||
|
@ -164,7 +167,7 @@ class StoryController extends Controller
|
|||
->map(function($s, $k) {
|
||||
return [
|
||||
'id' => (string) $s->id,
|
||||
'type' => 'photo',
|
||||
'type' => Str::endsWith($s->path, '.mp4') ? 'video' :'photo',
|
||||
'length' => 3,
|
||||
'src' => url(Storage::url($s->path)),
|
||||
'preview' => null,
|
||||
|
@ -198,7 +201,7 @@ class StoryController extends Controller
|
|||
|
||||
$res = [
|
||||
'id' => (string) $story->id,
|
||||
'type' => 'photo',
|
||||
'type' => Str::endsWith($story->path, '.mp4') ? 'video' :'photo',
|
||||
'length' => 3,
|
||||
'src' => url(Storage::url($story->path)),
|
||||
'preview' => null,
|
||||
|
@ -233,7 +236,7 @@ class StoryController extends Controller
|
|||
->map(function($s, $k) {
|
||||
return [
|
||||
'id' => $s->id,
|
||||
'type' => 'photo',
|
||||
'type' => Str::endsWith($s->path, '.mp4') ? 'video' :'photo',
|
||||
'length' => 3,
|
||||
'src' => url(Storage::url($s->path)),
|
||||
'preview' => null,
|
||||
|
@ -315,7 +318,7 @@ class StoryController extends Controller
|
|||
->map(function($s, $k) {
|
||||
return [
|
||||
'id' => $s->id,
|
||||
'type' => 'photo',
|
||||
'type' => Str::endsWith($s->path, '.mp4') ? 'video' :'photo',
|
||||
'length' => 3,
|
||||
'src' => url(Storage::url($s->path)),
|
||||
'preview' => null,
|
||||
|
|
13
app/MediaTag.php
Normal file
13
app/MediaTag.php
Normal file
|
@ -0,0 +1,13 @@
|
|||
<?php
|
||||
|
||||
namespace App;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class MediaTag extends Model
|
||||
{
|
||||
public function status()
|
||||
{
|
||||
return $this->belongsTo(Status::class);
|
||||
}
|
||||
}
|
|
@ -14,7 +14,7 @@ class ActivityPubFetchService
|
|||
public $url;
|
||||
public $headers = [
|
||||
'Accept' => 'application/activity+json, application/json',
|
||||
'User-Agent' => 'PixelfedBot - https://pixelfed.org'
|
||||
'User-Agent' => '(Pixelfed/'.config('pixelfed.version').'; +'.config('app.url').')'
|
||||
];
|
||||
|
||||
public static function queue()
|
||||
|
|
50
app/Services/HashidService.php
Normal file
50
app/Services/HashidService.php
Normal file
|
@ -0,0 +1,50 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use Cache;
|
||||
|
||||
class HashidService {
|
||||
|
||||
public const MIN_LIMIT = 15;
|
||||
public const CMAP = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';
|
||||
|
||||
public static function encode($id)
|
||||
{
|
||||
if(!is_numeric($id) || $id > PHP_INT_MAX || strlen($id) < self::MIN_LIMIT) {
|
||||
return null;
|
||||
}
|
||||
$key = "hashids:{$id}";
|
||||
return Cache::remember($key, now()->hours(48), function() use($id) {
|
||||
$cmap = self::CMAP;
|
||||
$base = strlen($cmap);
|
||||
$shortcode = '';
|
||||
while($id) {
|
||||
$id = ($id - ($r = $id % $base)) / $base;
|
||||
$shortcode = $cmap{$r} . $shortcode;
|
||||
};
|
||||
return $shortcode;
|
||||
});
|
||||
}
|
||||
|
||||
public static function decode($short)
|
||||
{
|
||||
$len = strlen($short);
|
||||
if($len < 3 || $len > 11) {
|
||||
return null;
|
||||
}
|
||||
$id = 0;
|
||||
foreach(str_split($short) as $needle) {
|
||||
$pos = strpos(self::CMAP, $needle);
|
||||
// if(!$pos) {
|
||||
// return null;
|
||||
// }
|
||||
$id = ($id*64) + $pos;
|
||||
}
|
||||
if(strlen($id) < self::MIN_LIMIT) {
|
||||
return null;
|
||||
}
|
||||
return $id;
|
||||
}
|
||||
|
||||
}
|
51
app/Services/MediaPathService.php
Normal file
51
app/Services/MediaPathService.php
Normal file
|
@ -0,0 +1,51 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Util\ActivityPub\Helpers;
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
use Illuminate\Support\Str;
|
||||
use App\Media;
|
||||
use App\Profile;
|
||||
use App\User;
|
||||
|
||||
class MediaPathService {
|
||||
|
||||
public static function get($account, $version = 1)
|
||||
{
|
||||
$mh = hash('sha256', date('Y').'-.-'.date('m'));
|
||||
|
||||
if($account instanceOf User) {
|
||||
switch ($version) {
|
||||
// deprecated
|
||||
case 1:
|
||||
$monthHash = hash('sha1', date('Y').date('m'));
|
||||
$userHash = hash('sha1', $account->id . (string) $account->created_at);
|
||||
$path = "public/m/{$monthHash}/{$userHash}";
|
||||
break;
|
||||
|
||||
case 2:
|
||||
$monthHash = substr($mh, 0, 9).'-'.substr($mh, 9, 6);
|
||||
$userHash = $account->profile_id;
|
||||
$random = Str::random(12);
|
||||
$path = "public/m/_v2/{$userHash}/{$monthHash}/{$random}";
|
||||
break;
|
||||
|
||||
default:
|
||||
$monthHash = substr($mh, 0, 9).'-'.substr($mh, 9, 6);
|
||||
$userHash = $account->profile_id;
|
||||
$random = Str::random(12);
|
||||
$path = "public/m/_v2/{$userHash}/{$monthHash}/{$random}";
|
||||
break;
|
||||
}
|
||||
}
|
||||
if($account instanceOf Profile) {
|
||||
$monthHash = substr($mh, 0, 9).'-'.substr($mh, 9, 6);
|
||||
$userHash = $account->id;
|
||||
$random = Str::random(12);
|
||||
$path = "public/m/_v2/{$userHash}/{$monthHash}/{$random}";
|
||||
}
|
||||
return $path;
|
||||
}
|
||||
|
||||
}
|
78
app/Services/MediaTagService.php
Normal file
78
app/Services/MediaTagService.php
Normal file
|
@ -0,0 +1,78 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use Cache;
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
use App\Notification;
|
||||
use App\MediaTag;
|
||||
use League\Fractal;
|
||||
use League\Fractal\Serializer\ArraySerializer;
|
||||
use League\Fractal\Pagination\IlluminatePaginatorAdapter;
|
||||
|
||||
class MediaTagService
|
||||
{
|
||||
const CACHE_KEY = 'pf:services:media_tags:id:';
|
||||
|
||||
public static function get($mediaId, $usernames = true)
|
||||
{
|
||||
$k = 'pf:services:media_tags:get:sid:' . $mediaId;
|
||||
return Cache::remember($k, now()->addMinutes(60), function() use($mediaId, $usernames) {
|
||||
$key = self::CACHE_KEY . $mediaId;
|
||||
if(Redis::zCount($key, '-inf', '+inf') == 0) {
|
||||
return [];
|
||||
}
|
||||
$res = Redis::zRange($key, 0, -1);
|
||||
if(!$usernames) {
|
||||
return $res;
|
||||
}
|
||||
$usernames = [];
|
||||
foreach ($res as $k) {
|
||||
$username = (new self)->idToUsername($k);
|
||||
array_push($usernames, $username);
|
||||
}
|
||||
|
||||
return $usernames;
|
||||
});
|
||||
}
|
||||
|
||||
public static function set($mediaId, $profileId)
|
||||
{
|
||||
$key = self::CACHE_KEY . $mediaId;
|
||||
Redis::zAdd($key, $profileId, $profileId);
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function idToUsername($id)
|
||||
{
|
||||
$profile = ProfileService::build()->profileId($id);
|
||||
|
||||
if(!$profile) {
|
||||
return 'unavailable';
|
||||
}
|
||||
|
||||
return [
|
||||
'username' => $profile->username,
|
||||
'avatar' => $profile->avatarUrl()
|
||||
];
|
||||
}
|
||||
|
||||
public static function sendNotification(MediaTag $tag)
|
||||
{
|
||||
$p = $tag->status->profile;
|
||||
$actor = $p->username;
|
||||
$message = "{$actor} tagged you in a post.";
|
||||
$rendered = "<a href='/{$actor}' class='profile-link'>{$actor}</a> tagged you in a post.";
|
||||
$n = new Notification;
|
||||
$n->profile_id = $tag->profile_id;
|
||||
$n->actor_id = $p->id;
|
||||
$n->item_id = $tag->id;
|
||||
$n->item_type = 'App\MediaTag';
|
||||
$n->action = 'tagged';
|
||||
$n->message = $message;
|
||||
$n->rendered = $rendered;
|
||||
$n->save();
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
|
@ -14,7 +14,8 @@ class NotificationTransformer extends Fractal\TransformerAbstract
|
|||
'account',
|
||||
'status',
|
||||
'relationship',
|
||||
'modlog'
|
||||
'modlog',
|
||||
'tagged'
|
||||
];
|
||||
|
||||
public function transform(Notification $notification)
|
||||
|
@ -55,7 +56,8 @@ class NotificationTransformer extends Fractal\TransformerAbstract
|
|||
'share' => 'share',
|
||||
'like' => 'favourite',
|
||||
'comment' => 'comment',
|
||||
'admin.user.modlog.comment' => 'modlog'
|
||||
'admin.user.modlog.comment' => 'modlog',
|
||||
'tagged' => 'tagged'
|
||||
];
|
||||
return $verbs[$verb];
|
||||
}
|
||||
|
@ -85,4 +87,22 @@ class NotificationTransformer extends Fractal\TransformerAbstract
|
|||
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->status->profile->username,
|
||||
'post_url' => $ml->status->url()
|
||||
];
|
||||
});
|
||||
return $res;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,6 +5,8 @@ namespace App\Transformer\Api;
|
|||
use App\Status;
|
||||
use League\Fractal;
|
||||
use Cache;
|
||||
use App\Services\HashidService;
|
||||
use App\Services\MediaTagService;
|
||||
|
||||
class StatusTransformer extends Fractal\TransformerAbstract
|
||||
{
|
||||
|
@ -15,12 +17,15 @@ class StatusTransformer extends Fractal\TransformerAbstract
|
|||
|
||||
public function transform(Status $status)
|
||||
{
|
||||
$taggedPeople = MediaTagService::get($status->id);
|
||||
|
||||
return [
|
||||
'id' => (string) $status->id,
|
||||
'shortcode' => HashidService::encode($status->id),
|
||||
'uri' => $status->url(),
|
||||
'url' => $status->url(),
|
||||
'in_reply_to_id' => $status->in_reply_to_id,
|
||||
'in_reply_to_account_id' => $status->in_reply_to_profile_id,
|
||||
'in_reply_to_id' => (string) $status->in_reply_to_id,
|
||||
'in_reply_to_account_id' => (string) $status->in_reply_to_profile_id,
|
||||
'reblog' => null,
|
||||
'content' => $status->rendered ?? $status->caption,
|
||||
'content_text' => $status->caption,
|
||||
|
@ -50,6 +55,7 @@ class StatusTransformer extends Fractal\TransformerAbstract
|
|||
'parent' => [],
|
||||
'place' => $status->place,
|
||||
'local' => (bool) $status->local,
|
||||
'taggedPeople' => $taggedPeople
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
@ -24,6 +24,7 @@ use App\Jobs\StatusPipeline\NewStatusPipeline;
|
|||
use App\Util\ActivityPub\HttpSignature;
|
||||
use Illuminate\Support\Str;
|
||||
use App\Services\ActivityPubDeliveryService;
|
||||
use App\Services\MediaPathService;
|
||||
|
||||
class Helpers {
|
||||
|
||||
|
@ -355,9 +356,7 @@ class Helpers {
|
|||
}
|
||||
$attachments = isset($data['object']) ? $data['object']['attachment'] : $data['attachment'];
|
||||
$user = $status->profile;
|
||||
$monthHash = hash('sha1', date('Y').date('m'));
|
||||
$userHash = hash('sha1', $user->id.(string) $user->created_at);
|
||||
$storagePath = "public/m/{$monthHash}/{$userHash}";
|
||||
$storagePath = MediaPathService::get($user, 2);
|
||||
$allowed = explode(',', config('pixelfed.media_types'));
|
||||
|
||||
foreach($attachments as $media) {
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreateMediaTagsTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('media_tags', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->bigInteger('status_id')->unsigned()->index()->nullable();
|
||||
$table->bigInteger('media_id')->unsigned()->index();
|
||||
$table->bigInteger('profile_id')->unsigned()->index();
|
||||
$table->string('tagged_username')->nullable();
|
||||
$table->boolean('is_public')->default(true)->index();
|
||||
$table->json('metadata')->nullable();
|
||||
$table->unique(['media_id', 'profile_id']);
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('media_tags');
|
||||
}
|
||||
}
|
BIN
public/js/activity.js
vendored
BIN
public/js/activity.js
vendored
Binary file not shown.
BIN
public/js/compose.js
vendored
BIN
public/js/compose.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/story-compose.js
vendored
BIN
public/js/story-compose.js
vendored
Binary file not shown.
BIN
public/js/timeline.js
vendored
BIN
public/js/timeline.js
vendored
Binary file not shown.
Binary file not shown.
|
@ -42,6 +42,16 @@
|
|||
<a :href="n.account.url" class="font-weight-bold text-dark word-break" data-placement="bottom" data-toggle="tooltip" :title="n.account.username">{{truncate(n.account.username)}}</a> shared your <a class="font-weight-bold" v-bind:href="n.status.reblog.url">post</a>.
|
||||
</p>
|
||||
</div>
|
||||
<div v-else-if="n.type == 'modlog'">
|
||||
<p class="my-0">
|
||||
<a :href="n.account.url" 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="n.account.url" class="font-weight-bold text-dark word-break" :title="n.account.username">{{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 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>
|
||||
|
@ -236,6 +246,9 @@ export default {
|
|||
case 'comment':
|
||||
return n.status.url;
|
||||
break;
|
||||
case 'tagged':
|
||||
return n.tagged.post_url;
|
||||
break;
|
||||
}
|
||||
return '/';
|
||||
},
|
||||
|
|
|
@ -254,9 +254,9 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <div class="border-bottom">
|
||||
<p class="px-4 mb-0 py-2 cursor-pointer" @click="showTagCard()">Tag people</p>
|
||||
</div> -->
|
||||
<div class="border-bottom">
|
||||
<p class="px-4 mb-0 py-2 cursor-pointer" @click="showTagCard()">Tag people <span class="ml-2 badge badge-primary">NEW</span></p>
|
||||
</div>
|
||||
<div class="border-bottom">
|
||||
<p class="px-4 mb-0 py-2 cursor-pointer" @click="showLocationCard()" v-if="!place">Add location</p>
|
||||
<p v-else class="px-4 mb-0 py-2">
|
||||
|
@ -269,9 +269,10 @@
|
|||
</div>
|
||||
<div class="border-bottom">
|
||||
<p class="px-4 mb-0 py-2">
|
||||
<span class="text-lighter">Visibility:</span> {{visibilityTag}}
|
||||
<span>Audience</span>
|
||||
<span class="float-right">
|
||||
<a v-if="profile.locked == false" href="#" @click.prevent="showVisibilityCard()" class="btn btn-outline-secondary btn-sm small mr-2" style="font-size:10px;padding:3px;text-transform: uppercase">Edit</a>
|
||||
<a v-if="profile.locked == false" href="#" @click.prevent="showVisibilityCard()" class="btn btn-outline-secondary btn-sm small mr-3 mt-n1 disabled" style="font-size:10px;padding:3px;text-transform: uppercase" disabled>{{visibilityTag}}</a>
|
||||
<a href="#" @click.prevent="showVisibilityCard()" class="text-decoration-none"><i class="fas fa-chevron-right fa-lg text-lighter"></i></a>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
|
@ -293,7 +294,42 @@
|
|||
</div>
|
||||
|
||||
<div v-if="page == 'tagPeople'" class="w-100 h-100 p-3">
|
||||
<p class="text-center lead text-muted mb-0 py-5">This feature is not available yet.</p>
|
||||
<autocomplete
|
||||
v-show="taggedUsernames.length < 10"
|
||||
:search="tagSearch"
|
||||
placeholder="@pixelfed"
|
||||
aria-label="Search usernames"
|
||||
:get-result-value="getTagResultValue"
|
||||
@submit="onTagSubmitLocation"
|
||||
ref="autocomplete"
|
||||
>
|
||||
</autocomplete>
|
||||
<p v-show="taggedUsernames.length < 10" class="font-weight-bold text-muted small">You can tag {{10 - taggedUsernames.length}} more {{taggedUsernames.length == 9 ? 'person' : 'people'}}!</p>
|
||||
<p class="font-weight-bold text-center mt-3">Tagged People</p>
|
||||
<div class="list-group">
|
||||
<div v-for="(tag, index) in taggedUsernames" class="list-group-item d-flex justify-content-between">
|
||||
<div class="media">
|
||||
<img class="mr-2 rounded-circle border" :src="tag.avatar" width="24px" height="24px">
|
||||
<div class="media-body">
|
||||
<span class="font-weight-bold">{{tag.name}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="custom-control custom-switch">
|
||||
<input type="checkbox" class="custom-control-input disabled" :id="'cci-tagged-privacy-switch'+index" v-model="tag.privacy" disabled>
|
||||
<label class="custom-control-label font-weight-bold text-lighter" :for="'cci-tagged-privacy-switch'+index">{{tag.privacy ? 'Public' : 'Private'}}</label>
|
||||
<a href="#" @click.prevent="untagUsername(index)" class="ml-3"><i class="fas fa-times text-muted"></i></a></div>
|
||||
</div>
|
||||
<div v-if="taggedUsernames.length == 0" class="list-group-item p-3">
|
||||
<p class="text-center mb-0 font-weight-bold text-lighter">Search usernames to tag.</p>
|
||||
</div>
|
||||
</div>
|
||||
<p class="font-weight-bold text-center small text-muted pt-3 mb-0">When you tag someone, they are sent a notification.<br>For more information on tagging, <a href="#" class="text-primary" @click.prevent="showTagHelpCard()">click here</a>.</p>
|
||||
</div>
|
||||
<div v-if="page == 'tagPeopleHelp'" class="w-100 h-100 p-3">
|
||||
<p class="mb-0 text-center py-3 px-2 lead">Tagging someone is like mentioning them, with the option to make it private between you.</p>
|
||||
<p class="mb-3 py-3 px-2 font-weight-lighter">
|
||||
You can choose to tag someone in public or private mode. Public mode will allow others to see who you tagged in the post and private mode tagged users will not be shown to others.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-if="page == 'addLocation'" class="w-100 h-100 p-3">
|
||||
|
@ -538,6 +574,7 @@ export default {
|
|||
composeTextLength: 0,
|
||||
nsfw: false,
|
||||
filters: [],
|
||||
currentFilter: false,
|
||||
ids: [],
|
||||
media: [],
|
||||
carouselCursor: 0,
|
||||
|
@ -560,7 +597,6 @@ export default {
|
|||
zoom: 0
|
||||
},
|
||||
|
||||
taggedUsernames: false,
|
||||
namedPages: [
|
||||
'cropPhoto',
|
||||
'tagPeople',
|
||||
|
@ -573,9 +609,12 @@ export default {
|
|||
'mediaMetadata',
|
||||
'addToStory',
|
||||
'editMedia',
|
||||
'cameraRoll'
|
||||
'cameraRoll',
|
||||
'tagPeopleHelp'
|
||||
],
|
||||
cameraRollMedia: []
|
||||
cameraRollMedia: [],
|
||||
taggedUsernames: [],
|
||||
taggedPeopleSearch: null
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -673,6 +712,7 @@ export default {
|
|||
|
||||
toggleFilter(e, filter) {
|
||||
this.media[this.carouselCursor].filter_class = filter;
|
||||
this.currentFilter = filter;
|
||||
},
|
||||
|
||||
deleteMedia() {
|
||||
|
@ -727,7 +767,8 @@ export default {
|
|||
visibility: this.visibility,
|
||||
cw: this.nsfw,
|
||||
comments_disabled: this.commentsDisabled,
|
||||
place: this.place
|
||||
place: this.place,
|
||||
tagged: this.taggedUsernames
|
||||
};
|
||||
axios.post('/api/local/status/compose', data)
|
||||
.then(res => {
|
||||
|
@ -766,6 +807,10 @@ export default {
|
|||
this.page = 2;
|
||||
break;
|
||||
|
||||
case 'tagPeopleHelp':
|
||||
this.showTagCard();
|
||||
break;
|
||||
|
||||
default:
|
||||
this.namedPages.indexOf(this.page) != -1 ? this.page = 3 : this.page--;
|
||||
break;
|
||||
|
@ -803,6 +848,15 @@ export default {
|
|||
break;
|
||||
|
||||
case 2:
|
||||
if(this.currentFilter) {
|
||||
if(window.confirm('Are you sure you want to apply this filter?')) {
|
||||
this.applyFilterToMedia();
|
||||
this.page++;
|
||||
}
|
||||
} else {
|
||||
this.page++;
|
||||
}
|
||||
break;
|
||||
case 3:
|
||||
this.page++;
|
||||
break;
|
||||
|
@ -823,6 +877,11 @@ export default {
|
|||
this.page = 'tagPeople';
|
||||
},
|
||||
|
||||
showTagHelpCard() {
|
||||
this.pageTitle = 'About Tag People';
|
||||
this.page = 'tagPeopleHelp';
|
||||
},
|
||||
|
||||
showLocationCard() {
|
||||
this.pageTitle = 'Add Location';
|
||||
this.page = 'addLocation';
|
||||
|
@ -909,7 +968,47 @@ export default {
|
|||
this.cameraRollMedia = res.data;
|
||||
});
|
||||
},
|
||||
applyFilterToMedia() {
|
||||
// this is where the magic happens
|
||||
|
||||
},
|
||||
|
||||
tagSearch(input) {
|
||||
if (input.length < 1) { return []; };
|
||||
let self = this;
|
||||
let results = [];
|
||||
return axios.get('/api/local/compose/tag/search', {
|
||||
params: {
|
||||
q: input
|
||||
}
|
||||
}).then(res => {
|
||||
//return res.data;
|
||||
return res.data.filter(d => {
|
||||
return self.taggedUsernames.filter(r => {
|
||||
return r.id == d.id;
|
||||
}).length == 0;
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
getTagResultValue(result) {
|
||||
return '@' + result.name;
|
||||
},
|
||||
|
||||
onTagSubmitLocation(result) {
|
||||
if(this.taggedUsernames.filter(r => {
|
||||
return r.id == result.id;
|
||||
}).length) {
|
||||
return;
|
||||
}
|
||||
this.taggedUsernames.push(result);
|
||||
this.$refs.autocomplete.value = '';
|
||||
return;
|
||||
},
|
||||
|
||||
untagUsername(index) {
|
||||
this.taggedUsernames.splice(index, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -57,6 +57,16 @@
|
|||
<a :href="n.account.url" 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="n.account.url" class="font-weight-bold text-dark word-break" :title="n.account.username">{{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>
|
||||
<p class="my-0">
|
||||
We cannot display this notification at this time.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="small text-muted font-weight-bold" :title="n.created_at">{{timeAgo(n.created_at)}}</div>
|
||||
</div>
|
||||
|
|
|
@ -80,7 +80,7 @@
|
|||
|
||||
<div class="col-12 col-md-4 px-0 d-flex flex-column border-left border-md-left-0">
|
||||
<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" data-toggle="tooltip" data-placement="bottom" :title="statusUsername">
|
||||
<div class="d-flex align-items-center status-username text-truncate">
|
||||
<div class="status-avatar mr-2" @click="redirect(statusProfileUrl)">
|
||||
<img :src="statusAvatar" width="24px" height="24px" style="border-radius:12px;" class="cursor-pointer">
|
||||
</div>
|
||||
|
@ -90,65 +90,73 @@
|
|||
<i class="fas fa-certificate text-danger fa-stack-1x"></i>
|
||||
<i class="fas fa-crown text-white fa-sm fa-stack-1x" style="font-size:7px;"></i>
|
||||
</span>
|
||||
<p v-if="loaded && status.place != null" class="small mb-0 cursor-pointer text-truncate" style="color:#718096" @click="redirect('/discover/places/' + status.place.id + '/' + status.place.slug)">{{status.place.name}}, {{status.place.country}}</p>
|
||||
<p class="mb-0" style="font-size: 10px;">
|
||||
<span v-if="loaded && status.taggedPeople.length" class="mb-0">
|
||||
<span class="font-weight-light cursor-pointer" style="color:#718096" title="Tagged People" data-toggle="tooltip" data-placement="bottom" @click="showTaggedPeopleModal()"><i class="fas fa-tag text-lighter"></i> <span class="font-weight-bold">{{status.taggedPeople.length}} Tagged People</span></span>
|
||||
</span>
|
||||
<span v-if="loaded && status.place != null && status.taggedPeople.length" class="px-2 font-weight-bold text-lighter">•</span>
|
||||
<span v-if="loaded && status.place != null" class="mb-0 cursor-pointer text-truncate" style="color:#718096" @click="redirect('/discover/places/' + status.place.id + '/' + status.place.slug)"><i class="fas fa-map-marked-alt text-lighter"></i> <span class="font-weight-bold">{{status.place.name}}, {{status.place.country}}</span></span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="float-right">
|
||||
<div class="post-actions">
|
||||
<div v-if="user != false" class="dropdown">
|
||||
<button class="btn btn-link text-dark no-caret dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" title="Post options">
|
||||
<span class="fas fa-ellipsis-v text-muted"></span>
|
||||
</button>
|
||||
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownMenuButton">
|
||||
<a class="dropdown-item font-weight-bold" @click="showEmbedPostModal()">Embed</a>
|
||||
<span v-if="!owner()">
|
||||
<a class="dropdown-item font-weight-bold" :href="reportUrl()">Report</a>
|
||||
<a class="dropdown-item font-weight-bold" v-on:click="muteProfile">Mute Profile</a>
|
||||
<a class="dropdown-item font-weight-bold" v-on:click="blockProfile">Block Profile</a>
|
||||
</span>
|
||||
<span v-if="ownerOrAdmin()">
|
||||
<a class="dropdown-item font-weight-bold" href="#" v-on:click.prevent="toggleCommentVisibility">{{ showComments ? 'Disable' : 'Enable'}} Comments</a>
|
||||
<a v-if="canEdit" class="dropdown-item font-weight-bold" :href="editUrl()">Edit</a>
|
||||
<a class="dropdown-item font-weight-bold text-danger" v-on:click="deletePost">Delete</a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="float-right">
|
||||
<div class="post-actions">
|
||||
<div v-if="user != false" class="dropdown">
|
||||
<button class="btn btn-link text-dark no-caret dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" title="Post options">
|
||||
<span class="fas fa-ellipsis-v text-muted"></span>
|
||||
</button>
|
||||
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownMenuButton">
|
||||
<a class="dropdown-item font-weight-bold" @click="showEmbedPostModal()">Embed</a>
|
||||
<span v-if="!owner()">
|
||||
<a class="dropdown-item font-weight-bold" :href="reportUrl()">Report</a>
|
||||
<a class="dropdown-item font-weight-bold" v-on:click="muteProfile">Mute Profile</a>
|
||||
<a class="dropdown-item font-weight-bold" v-on:click="blockProfile">Block Profile</a>
|
||||
</span>
|
||||
<span v-if="ownerOrAdmin()">
|
||||
<a class="dropdown-item font-weight-bold" href="#" v-on:click.prevent="toggleCommentVisibility">{{ showComments ? 'Disable' : 'Enable'}} Comments</a>
|
||||
<a v-if="canEdit" class="dropdown-item font-weight-bold" :href="editUrl()">Edit</a>
|
||||
<a class="dropdown-item font-weight-bold text-danger" v-on:click="deletePost">Delete</a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex flex-md-column flex-column-reverse h-100" style="overflow-y: auto;">
|
||||
<div class="card-body status-comments pb-5">
|
||||
<div class="card-body status-comments pb-5 pt-0">
|
||||
<div class="status-comment">
|
||||
<div v-if="showCaption != true">
|
||||
<span class="py-3">
|
||||
<a class="text-dark font-weight-bold mr-1" :href="status.account.url" v-bind:title="status.account.username">{{truncate(status.account.username,15)}}</a>
|
||||
<span class="text-break">
|
||||
<span class="font-italic text-muted">This comment may contain sensitive material</span>
|
||||
<span class="text-primary cursor-pointer pl-1" @click="showCaption = true">Show</span>
|
||||
<div v-if="status.content.length" class="pt-3">
|
||||
<div v-if="showCaption != true">
|
||||
<span class="py-3">
|
||||
<a class="text-dark font-weight-bold mr-1" :href="status.account.url" v-bind:title="status.account.username">{{truncate(status.account.username,15)}}</a>
|
||||
<span class="text-break">
|
||||
<span class="font-italic text-muted">This comment may contain sensitive material</span>
|
||||
<span class="text-primary cursor-pointer pl-1" @click="showCaption = true">Show</span>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div v-else>
|
||||
<p :class="[status.content.length > 620 ? 'mb-1 read-more' : 'mb-1']" style="overflow: hidden;">
|
||||
<a class="font-weight-bold pr-1 text-dark text-decoration-none" :href="statusProfileUrl">{{statusUsername}}</a>
|
||||
<span class="comment-text" :id="status.id + '-status-readmore'" v-html="status.content"></span>
|
||||
</p>
|
||||
</div>
|
||||
<div v-else>
|
||||
<p :class="[status.content.length > 620 ? 'mb-1 read-more' : 'mb-1']" style="overflow: hidden;">
|
||||
<a class="font-weight-bold pr-1 text-dark text-decoration-none" :href="statusProfileUrl">{{statusUsername}}</a>
|
||||
<span class="comment-text" :id="status.id + '-status-readmore'" v-html="status.content"></span>
|
||||
</p>
|
||||
</div>
|
||||
<hr>
|
||||
</div>
|
||||
|
||||
<div v-if="showComments">
|
||||
<hr>
|
||||
<div class="postCommentsLoader text-center py-2">
|
||||
<div class="spinner-border" role="status">
|
||||
<span class="sr-only">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="postCommentsContainer d-none">
|
||||
<p class="mb-1 text-center load-more-link d-none my-3">
|
||||
<p class="mb-1 text-center load-more-link d-none my-4">
|
||||
<a href="#" class="text-dark" v-on:click="loadMore" title="Load more comments" data-toggle="tooltip" data-placement="bottom">
|
||||
<svg class="bi bi-plus-circle" width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg" style="font-size:2em;"> <path fill-rule="evenodd" d="M8 3.5a.5.5 0 01.5.5v4a.5.5 0 01-.5.5H4a.5.5 0 010-1h3.5V4a.5.5 0 01.5-.5z" clip-rule="evenodd"/> <path fill-rule="evenodd" d="M7.5 8a.5.5 0 01.5-.5h4a.5.5 0 010 1H8.5V12a.5.5 0 01-1 0V8z" clip-rule="evenodd"/> <path fill-rule="evenodd" d="M8 15A7 7 0 108 1a7 7 0 000 14zm0 1A8 8 0 108 0a8 8 0 000 16z" clip-rule="evenodd"/></svg>
|
||||
</a>
|
||||
</p>
|
||||
<div class="comments">
|
||||
<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">
|
||||
<div class="media-body">
|
||||
|
@ -216,8 +224,9 @@
|
|||
<h3 v-bind:class="[reactions.liked ? 'fas fa-heart text-danger pr-3 m-0 cursor-pointer' : 'far fa-heart pr-3 m-0 like-btn cursor-pointer']" title="Like" v-on:click="likeStatus"></h3>
|
||||
<h3 v-if="!status.comments_disabled" class="far fa-comment pr-3 m-0 cursor-pointer" title="Comment" v-on:click="replyFocus(status)"></h3>
|
||||
<h3 v-if="status.visibility == 'public'" v-bind:class="[reactions.shared ? 'far fa-share-square pr-3 m-0 text-primary cursor-pointer' : 'far fa-share-square pr-3 m-0 share-btn cursor-pointer']" title="Share" v-on:click="shareStatus"></h3>
|
||||
<h3 @click="lightbox(status.media_attachments[0])" class="fas fa-expand m-0 cursor-pointer"></h3>
|
||||
<h3 v-if="status.visibility == 'public'" v-bind:class="[reactions.bookmarked ? 'fas fa-bookmark text-warning m-0 float-right cursor-pointer' : 'far fa-bookmark m-0 float-right cursor-pointer']" title="Bookmark" v-on:click="bookmarkStatus"></h3>
|
||||
<!-- <h3 @click="lightbox(status.media_attachments[0])" class="fas fa-expand m-0 cursor-pointer"></h3>
|
||||
<h3 v-if="status.visibility == 'public'" v-bind:class="[reactions.bookmarked ? 'fas fa-bookmark text-warning m-0 float-right cursor-pointer' : 'far fa-bookmark m-0 float-right cursor-pointer']" title="Bookmark" v-on:click="bookmarkStatus"></h3> -->
|
||||
<h3 v-if="status.visibility == 'public'" v-bind:class="[reactions.bookmarked ? 'fas fa-bookmark text-warning m-0 cursor-pointer' : 'far fa-bookmark m-0 cursor-pointer']" title="Bookmark" v-on:click="bookmarkStatus"></h3>
|
||||
</div>
|
||||
<div class="reaction-counts font-weight-bold mb-0">
|
||||
<span style="cursor:pointer;" v-on:click="likesModal">
|
||||
|
@ -235,11 +244,11 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="showComments && user.length !== 0" class="card-footer bg-white px-2 py-0">
|
||||
<!-- <div v-if="showComments && user.length !== 0" class="card-footer bg-white px-2 py-0">
|
||||
<ul class="nav align-items-center emoji-reactions" style="overflow-x: scroll;flex-wrap: unset;">
|
||||
<li class="nav-item" v-on:click="emojiReaction" v-for="e in emoji">{{e}}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div> -->
|
||||
<div v-if="showComments" class="card-footer bg-white sticky-md-bottom p-0">
|
||||
<div v-if="user.length == 0" class="comment-form-guest p-3">
|
||||
<a href="/login">Login</a> to like or comment.
|
||||
|
@ -253,11 +262,11 @@
|
|||
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="showProfileMorePosts">
|
||||
<div class="py-4">
|
||||
<div class="container" v-if="showProfileMorePosts">
|
||||
<!-- <div class="py-4">
|
||||
<hr>
|
||||
</div>
|
||||
<p class="text-lighter px-3" style="font-weight: 600;font-size: 15px;">More posts from <a :href="'/'+statusUsername" class="text-dark">{{this.statusUsername}}</a></p>
|
||||
</div> -->
|
||||
<p class="text-lighter px-3 mt-5" style="font-weight: 600;font-size: 15px;">More posts from <a :href="'/'+statusUsername" class="text-dark">{{this.statusUsername}}</a></p>
|
||||
<div class="profile-timeline mt-md-4">
|
||||
<div class="row">
|
||||
<div class="col-4 p-1 p-md-3" v-for="(s, index) in profileMorePosts" :key="'tlob:'+index">
|
||||
|
@ -573,6 +582,30 @@
|
|||
<p class="mb-0 px-2 small text-muted">By using this embed, you agree to our <a href="/site/terms">Terms of Use</a></p>
|
||||
</div>
|
||||
</b-modal>
|
||||
<b-modal ref="taggedModal"
|
||||
id="tagged-modal"
|
||||
hide-footer
|
||||
centered
|
||||
title="Tagged People"
|
||||
body-class="list-group-flush py-3 px-0">
|
||||
<div class="list-group">
|
||||
<div class="list-group-item border-0 py-1" v-for="(user, index) in status.taggedPeople" :key="'modal_taggedpeople_'+index">
|
||||
<div class="media">
|
||||
<a :href="'/'+user.username">
|
||||
<img class="mr-3 rounded-circle box-shadow" :src="user.avatar" :alt="user.username + '’s avatar'" width="30px">
|
||||
</a>
|
||||
<div class="media-body">
|
||||
<p class="pt-1" style="font-size: 14px">
|
||||
<a :href="'/'+user.username" class="font-weight-bold text-dark">
|
||||
{{user.username}}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="mb-0 text-center small text-muted font-weight-bold"><a href="/site/kb/tagging-people">Learn more</a> about Tagging People.</p>
|
||||
</b-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -760,7 +793,7 @@ export default {
|
|||
|
||||
updated() {
|
||||
$('.carousel').carousel();
|
||||
// $('[data-toggle="tooltip"]').tooltip();
|
||||
$('[data-toggle="tooltip"]').tooltip();
|
||||
if(this.showReadMore == true) {
|
||||
window.pixelfed.readmore();
|
||||
}
|
||||
|
@ -1351,6 +1384,10 @@ export default {
|
|||
return '/i/web/post/_/' + status.account.id + '/' + status.id;
|
||||
},
|
||||
|
||||
showTaggedPeopleModal() {
|
||||
this.$refs.taggedModal.show();
|
||||
}
|
||||
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -33,7 +33,7 @@
|
|||
</span>
|
||||
</div>
|
||||
<p class="pl-2 h4 font-weight-bold mb-1">{{profile.display_name}}</p>
|
||||
<p class="pl-2 font-weight-bold mb-2 text-muted">{{profile.acct}}</p>
|
||||
<p class="pl-2 font-weight-bold mb-2"><a class="text-muted" :href="profile.url" @click.prevent="urlRedirectHandler(profile.url)">{{profile.acct}}</a></p>
|
||||
<p class="pl-2 text-muted small d-flex justify-content-between">
|
||||
<span>
|
||||
<span class="font-weight-bold text-dark">{{profile.statuses_count}}</span>
|
||||
|
@ -481,6 +481,18 @@
|
|||
suffix = suffix ? ' ' + suffix : '';
|
||||
return App.util.format.timeAgo(ts) + suffix;
|
||||
},
|
||||
|
||||
urlRedirectHandler(url) {
|
||||
let p = new URL(url);
|
||||
let path = '';
|
||||
if(p.hostname == window.location.hostname) {
|
||||
path = url;
|
||||
} else {
|
||||
path = '/i/redirect?url=';
|
||||
path += encodeURI(url);
|
||||
}
|
||||
window.location.href = path;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<div class="container mt-2 mt-md-5">
|
||||
<input type="file" id="pf-dz" name="media" class="d-none file-input" v-bind:accept="config.mimes">
|
||||
<div class="row">
|
||||
<div v-if="loaded" class="row">
|
||||
<div class="col-12 col-md-6 offset-md-3">
|
||||
|
||||
<!-- LANDING -->
|
||||
|
@ -9,12 +9,19 @@
|
|||
<div class="text-center flex-fill mt-5 pt-5">
|
||||
<img src="/img/pixelfed-icon-grey.svg" width="60px" height="60px">
|
||||
<p class="font-weight-bold lead text-lighter mt-1">Stories</p>
|
||||
<!-- <p v-if="loaded" class="font-weight-bold small text-uppercase text-muted">
|
||||
<span>{{stories.length}} Active</span>
|
||||
<span class="px-2">|</span>
|
||||
<span>30K Views</span>
|
||||
</p> -->
|
||||
</div>
|
||||
<div class="flex-fill">
|
||||
<div class="flex-fill py-4">
|
||||
<div class="card w-100 shadow-none">
|
||||
<div class="list-group">
|
||||
<!-- <a class="list-group-item text-center lead text-decoration-none text-dark" href="#">Camera</a> -->
|
||||
<a class="list-group-item text-center lead text-decoration-none text-dark" href="#" @click.prevent="upload()">Add Photo</a>
|
||||
<a v-if="stories.length" class="list-group-item text-center lead text-decoration-none text-dark" href="#" @click.prevent="edit()">Edit Story</a>
|
||||
<a v-if="stories.length" class="list-group-item text-center lead text-decoration-none text-dark" href="#" @click.prevent="edit()">Edit</a>
|
||||
<!-- <a class="list-group-item text-center lead text-decoration-none text-dark" href="#">Options</a> -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -150,10 +157,12 @@
|
|||
props: ['profile-id'],
|
||||
data() {
|
||||
return {
|
||||
loaded: false,
|
||||
config: window.App.config,
|
||||
mimes: [
|
||||
'image/jpeg',
|
||||
'image/png'
|
||||
'image/png',
|
||||
// 'video/mp4'
|
||||
],
|
||||
page: 'landing',
|
||||
pages: [
|
||||
|
@ -181,7 +190,10 @@
|
|||
mounted() {
|
||||
this.mediaWatcher();
|
||||
axios.get('/api/stories/v0/fetch/' + this.profileId)
|
||||
.then(res => this.stories = res.data);
|
||||
.then(res => {
|
||||
this.stories = res.data;
|
||||
this.loaded = true;
|
||||
});
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
|
|
@ -91,13 +91,14 @@
|
|||
|
||||
<div class="card mb-sm-4 status-card card-md-rounded-0 shadow-none border">
|
||||
<div v-if="!modes.distractionFree && status" class="card-header d-inline-flex align-items-center bg-white">
|
||||
<img v-bind:src="status.account.avatar" width="38px" height="38px" class="cursor-pointer" style="border-radius: 38px;" @click="profileUrl(status)" onerror="this.onerror=null;this.src='/storage/avatars/default.png?v=2'">
|
||||
<!-- <img v-bind:src="status.account.avatar" width="38px" height="38px" class="cursor-pointer" style="border-radius: 38px;" @click="profileUrl(status)" onerror="this.onerror=null;this.src='/storage/avatars/default.png?v=2'"> -->
|
||||
<!-- <div v-if="hasStory" class="has-story has-story-sm cursor-pointer shadow-sm" @click="profileUrl(status)">
|
||||
<img class="rounded-circle box-shadow" :src="status.account.avatar" width="32px" height="32px" onerror="this.onerror=null;this.src='/storage/avatars/default.png?v=2'">
|
||||
</div>
|
||||
<div v-else>
|
||||
<div v-else> -->
|
||||
<div>
|
||||
<img class="rounded-circle box-shadow" :src="status.account.avatar" width="32px" height="32px" onerror="this.onerror=null;this.src='/storage/avatars/default.png?v=2'">
|
||||
</div> -->
|
||||
</div>
|
||||
<div class="pl-2">
|
||||
<!-- <a class="d-block username font-weight-bold text-dark" v-bind:href="status.account.url" style="line-height:0.5;"> -->
|
||||
<a class="username font-weight-bold text-dark text-decoration-none" v-bind:href="profileUrl(status)" v-html="statusCardUsernameFormat(status)">
|
||||
|
@ -107,11 +108,17 @@
|
|||
<i class="fas fa-certificate text-danger fa-stack-1x"></i>
|
||||
<i class="fas fa-crown text-white fa-sm fa-stack-1x" style="font-size:7px;"></i>
|
||||
</span>
|
||||
<span v-if="scope != 'home' && status.account.id != profile.id && status.account.relationship">
|
||||
<!-- <span v-if="scope != 'home' && status.account.id != profile.id && status.account.relationship">
|
||||
<span class="px-1">•</span>
|
||||
<span :class="'font-weight-bold cursor-pointer ' + [status.account.relationship.following == true ? 'text-muted' : 'text-primary']" @click="followAction(status)">{{status.account.relationship.following == true ? 'Following' : 'Follow'}}</span>
|
||||
</span>
|
||||
<a v-if="status.place" class="d-block small text-decoration-none" :href="'/discover/places/'+status.place.id+'/'+status.place.slug" style="color:#718096">{{status.place.name}}, {{status.place.country}}</a>
|
||||
</span> -->
|
||||
<!-- <span v-if="status.account.id != profile.id">
|
||||
<span class="px-1">•</span>
|
||||
<span class="font-weight-bold cursor-pointer text-primary">Follow</span>
|
||||
</span> -->
|
||||
<div class="d-flex align-items-center">
|
||||
<a v-if="status.place" class="small text-decoration-none" :href="'/discover/places/'+status.place.id+'/'+status.place.slug" style="color:#718096" title="Location" data-toggle="tooltip"><i class="fas fa-map-marked-alt"></i> {{status.place.name}}, {{status.place.country}}</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-right" style="flex-grow:1;">
|
||||
<button class="btn btn-link text-dark py-0" type="button" @click="ctxMenu(status)">
|
||||
|
@ -151,8 +158,16 @@
|
|||
<h3 v-bind:class="[status.favourited ? 'fas fa-heart text-danger pr-3 m-0 cursor-pointer' : 'far fa-heart pr-3 m-0 like-btn text-lighter cursor-pointer']" title="Like" v-on:click="likeStatus(status, $event)"></h3>
|
||||
<h3 v-if="!status.comments_disabled" class="far fa-comment text-lighter pr-3 m-0 cursor-pointer" title="Comment" v-on:click="commentFocus(status, $event)"></h3>
|
||||
<h3 v-if="status.visibility == 'public'" v-bind:class="[status.reblogged ? 'fas fa-retweet pr-3 m-0 text-primary cursor-pointer' : 'fas fa-retweet pr-3 m-0 text-lighter share-btn cursor-pointer']" title="Share" v-on:click="shareStatus(status, $event)"></h3>
|
||||
<span v-if="status.pf_type == 'photo'" class="float-right">
|
||||
<h3 class="fas fa-expand pr-3 m-0 cursor-pointer text-lighter" v-on:click="lightbox(status)"></h3>
|
||||
<span v-if="status.taggedPeople.length" class="float-right">
|
||||
<!-- <h3 class="fas fa-expand pr-3 m-0 cursor-pointer text-lighter" v-on:click="lightbox(status)"></h3> -->
|
||||
<span class="font-weight-light small" style="color:#718096">
|
||||
<i class="far fa-user" data-toggle="tooltip" title="Tagged People"></i>
|
||||
<span v-for="(tag, index) in status.taggedPeople" class="mr-n2">
|
||||
<a :href="'/'+tag.username">
|
||||
<img :src="tag.avatar" width="20px" height="20px" class="border rounded-circle" data-toggle="tooltip" :title="'@'+tag.username">
|
||||
</a>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -21,6 +21,8 @@ return [
|
|||
'blockingAccounts' => 'Blocking Accounts',
|
||||
'safetyTips' => 'Safety Tips',
|
||||
'reportSomething' => 'Report Something',
|
||||
'dataPolicy' => 'Data Policy'
|
||||
'dataPolicy' => 'Data Policy',
|
||||
|
||||
'taggingPeople' => 'Tagging People'
|
||||
|
||||
];
|
|
@ -13,7 +13,7 @@ return [
|
|||
'currentLocale' => 'Obecny język',
|
||||
'selectLocale' => 'Wybierz jeden z dostępnych języków',
|
||||
'contact' => 'Kontakt',
|
||||
'contact-us' => 'Skontaktuj się z naim',
|
||||
'contact-us' => 'Skontaktuj się z nami',
|
||||
'places' => 'Miejsca',
|
||||
'profiles' => 'Profile',
|
||||
|
||||
|
|
|
@ -11,7 +11,6 @@
|
|||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
<script type="text/javascript" src="{{mix('js/compose.js')}}"></script>
|
||||
<script type="text/javascript" src="{{mix('js/profile-directory.js')}}"></script>
|
||||
<script type="text/javascript">App.boot();</script>
|
||||
@endpush
|
|
@ -1,4 +1,4 @@
|
|||
<nav class="navbar navbar-expand navbar-light navbar-laravel sticky-top">
|
||||
<nav class="navbar navbar-expand navbar-light navbar-laravel shadow-none border-bottom sticky-top">
|
||||
<div class="container">
|
||||
<a class="navbar-brand d-flex align-items-center" href="{{ url('/') }}" title="{{ config('app.name', 'Laravel') }} Logo">
|
||||
<img src="/img/pixelfed-icon-color.svg" height="30px" class="px-2">
|
||||
|
|
29
resources/views/site/help/tagging-people.blade.php
Normal file
29
resources/views/site/help/tagging-people.blade.php
Normal file
|
@ -0,0 +1,29 @@
|
|||
@extends('site.help.partial.template', ['breadcrumb'=>'Tagging People'])
|
||||
|
||||
@section('section')
|
||||
|
||||
<div class="title">
|
||||
<h3 class="font-weight-bold">Tagging People</h3>
|
||||
</div>
|
||||
<hr>
|
||||
<p class="lead">Tag people in your posts without mentioning them in the caption.</p>
|
||||
<div class="py-4">
|
||||
<p class="font-weight-bold h5 pb-3">Tagging People in Posts</p>
|
||||
<ul>
|
||||
<li class="mb-3 ">You can only tag <span class="font-weight-bold">local</span> and <span class="font-weight-bold">public</span> accounts who haven't blocked you.</li>
|
||||
<li class="mb-3 ">You can tag up to <span class="font-weight-bold">10</span> people.</li>
|
||||
</ul>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="card bg-primary border-primary" style="box-shadow: none !important;border: 3px solid #08d!important;">
|
||||
<div class="card-header text-light font-weight-bold h4 p-4 bg-primary">Tagging Tips</div>
|
||||
<div class="card-body bg-white p-3">
|
||||
<ul class="pt-3">
|
||||
<li class="lead mb-4">Tagging someone will send them a notification.</li>
|
||||
<li class="lead mb-4">You can untag yourself from posts.</li>
|
||||
<li class="lead ">Only tag people you know.</li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
|
@ -186,6 +186,8 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact
|
|||
|
||||
Route::post('compose/media/update/{id}', 'MediaController@composeUpdate')->middleware('throttle:maxComposeMediaUpdatesPerHour,60')->middleware('throttle:maxComposeMediaUpdatesPerDay,1440')->middleware('throttle:maxComposeMediaUpdatesPerMonth,43800');
|
||||
Route::get('compose/location/search', 'ApiController@composeLocationSearch');
|
||||
Route::get('compose/tag/search', 'MediaTagController@usernameLookup');
|
||||
|
||||
});
|
||||
Route::group(['prefix' => 'admin'], function () {
|
||||
Route::post('moderate', 'Api\AdminApiController@moderate');
|
||||
|
@ -274,6 +276,8 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact
|
|||
Route::get('job/{uuid}/3', 'ImportController@instagramStepThree');
|
||||
Route::post('job/{uuid}/3', 'ImportController@instagramStepThreeStore');
|
||||
});
|
||||
|
||||
Route::get('redirect', 'SiteController@redirectUrl');
|
||||
});
|
||||
|
||||
Route::group(['prefix' => 'account'], function () {
|
||||
|
@ -416,7 +420,7 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact
|
|||
Route::view('report-something', 'site.help.report-something')->name('help.report-something');
|
||||
Route::view('data-policy', 'site.help.data-policy')->name('help.data-policy');
|
||||
Route::view('labs-deprecation', 'site.help.labs-deprecation')->name('help.labs-deprecation');
|
||||
|
||||
Route::view('tagging-people', 'site.help.tagging-people')->name('help.tagging-people');
|
||||
});
|
||||
Route::get('newsroom/{year}/{month}/{slug}', 'NewsroomController@show');
|
||||
Route::get('newsroom/archive', 'NewsroomController@archive');
|
||||
|
@ -439,6 +443,7 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact
|
|||
});
|
||||
|
||||
Route::get('stories/{username}', 'ProfileController@stories');
|
||||
Route::get('p/{id}', 'StatusController@shortcodeRedirect');
|
||||
Route::get('c/{collection}', 'CollectionController@show');
|
||||
Route::get('p/{username}/{id}/c', 'CommentController@showAll');
|
||||
Route::get('p/{username}/{id}/embed', 'StatusController@showEmbed');
|
||||
|
|
Loading…
Reference in a new issue