Merge pull request #2324 from pixelfed/staging

Bug fixes
This commit is contained in:
daniel 2020-07-20 09:47:10 -06:00 committed by GitHub
commit 7353bc2d8c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
37 changed files with 694 additions and 108 deletions

View file

@ -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

View file

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

View file

@ -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;
}
/**

View file

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

View file

@ -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;

View 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;
}
}

View file

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

View file

@ -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
View 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);
}
}

View file

@ -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()

View 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;
}
}

View 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;
}
}

View 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;
}
}

View file

@ -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;
}
}
}

View file

@ -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
];
}

View file

@ -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) {

View file

@ -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

Binary file not shown.

BIN
public/js/compose.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.

Binary file not shown.

View file

@ -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 '/';
},

View file

@ -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>

View file

@ -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>

View file

@ -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">&#8226;</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>

View file

@ -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>

View file

@ -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: {

View file

@ -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>

View file

@ -21,6 +21,8 @@ return [
'blockingAccounts' => 'Blocking Accounts',
'safetyTips' => 'Safety Tips',
'reportSomething' => 'Report Something',
'dataPolicy' => 'Data Policy'
'dataPolicy' => 'Data Policy',
'taggingPeople' => 'Tagging People'
];

View file

@ -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',

View file

@ -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

View file

@ -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">

View 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

View file

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