Merge pull request #1565 from pixelfed/frontend-ui-refactor

Frontend ui refactor
This commit is contained in:
daniel 2019-08-05 21:46:40 -06:00 committed by GitHub
commit 73ecb2f48a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
43 changed files with 842 additions and 781 deletions

View file

@ -118,7 +118,7 @@ class BaseApiController extends Controller
$since_id = $request->since_id ?? false; $since_id = $request->since_id ?? false;
$only_media = $request->only_media ?? false; $only_media = $request->only_media ?? false;
$user = Auth::user(); $user = Auth::user();
$account = Profile::findOrFail($id); $account = Profile::whereNull('status')->findOrFail($id);
$statuses = $account->statuses()->getQuery(); $statuses = $account->statuses()->getQuery();
if($only_media == true) { if($only_media == true) {
$statuses = $statuses $statuses = $statuses
@ -150,15 +150,6 @@ class BaseApiController extends Controller
return response()->json($res); return response()->json($res);
} }
public function followSuggestions(Request $request)
{
$followers = Auth::user()->profile->recommendFollowers();
$resource = new Fractal\Resource\Collection($followers, new AccountTransformer());
$res = $this->fractal->createData($resource)->toArray();
return response()->json($res);
}
public function avatarUpdate(Request $request) public function avatarUpdate(Request $request)
{ {
$this->validate($request, [ $this->validate($request, [
@ -197,14 +188,9 @@ class BaseApiController extends Controller
public function showTempMedia(Request $request, int $profileId, $mediaId) public function showTempMedia(Request $request, int $profileId, $mediaId)
{ {
if (!$request->hasValidSignature()) { abort_if(!$request->hasValidSignature(), 404);
abort(401); abort_if(Auth::user()->profile_id !== $profileId, 404);
} $media = Media::whereProfileId(Auth::user()->profile_id)->findOrFail($mediaId);
$profile = Auth::user()->profile;
if($profile->id !== $profileId) {
abort(403);
}
$media = Media::whereProfileId($profile->id)->findOrFail($mediaId);
$path = storage_path('app/'.$media->media_path); $path = storage_path('app/'.$media->media_path);
return response()->file($path); return response()->file($path);
} }

View file

@ -10,6 +10,7 @@ use App\{
UserFilter UserFilter
}; };
use Auth, Cache, Redis; use Auth, Cache, Redis;
use App\Util\Site\Config;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use App\Services\SuggestionService; use App\Services\SuggestionService;
@ -23,34 +24,7 @@ class ApiController extends BaseApiController
public function siteConfiguration(Request $request) public function siteConfiguration(Request $request)
{ {
$res = Cache::remember('api:site:configuration', now()->addMinutes(30), function() { return response()->json(Config::get());
return [
'uploader' => [
'max_photo_size' => config('pixelfed.max_photo_size'),
'max_caption_length' => config('pixelfed.max_caption_length'),
'album_limit' => config('pixelfed.max_album_length'),
'image_quality' => config('pixelfed.image_quality'),
'optimize_image' => config('pixelfed.optimize_image'),
'optimize_video' => config('pixelfed.optimize_video'),
'media_types' => config('pixelfed.media_types'),
'enforce_account_limit' => config('pixelfed.enforce_account_limit')
],
'activitypub' => [
'enabled' => config('federation.activitypub.enabled'),
'remote_follow' => config('federation.activitypub.remoteFollow')
],
'ab' => [
'lc' => config('exp.lc'),
'rec' => config('exp.rec'),
'loops' => config('exp.loops')
],
];
});
return response()->json($res);
} }
public function userRecommendations(Request $request) public function userRecommendations(Request $request)

View file

@ -34,7 +34,7 @@ class CollectionController extends Controller
public function show(Request $request, int $collection) public function show(Request $request, int $collection)
{ {
$collection = Collection::whereNotNull('published_at')->findOrFail($collection); $collection = Collection::with('profile')->whereNotNull('published_at')->findOrFail($collection);
if($collection->profile->status != null) { if($collection->profile->status != null) {
abort(404); abort(404);
} }
@ -100,7 +100,11 @@ class CollectionController extends Controller
$collection->items()->delete(); $collection->items()->delete();
$collection->delete(); $collection->delete();
return 200; if($request->wantsJson()) {
return 200;
}
return redirect('/');
} }
public function storeId(Request $request) public function storeId(Request $request)

View file

@ -245,4 +245,10 @@ class ProfileController extends Controller
} }
return view('profile.following', compact('user', 'profile', 'following', 'owner', 'is_following', 'is_admin', 'settings')); return view('profile.following', compact('user', 'profile', 'following', 'owner', 'is_following', 'is_admin', 'settings'));
} }
public function meRedirect()
{
abort_if(!Auth::check(), 404);
return redirect(Auth::user()->url());
}
} }

View file

@ -272,6 +272,7 @@ class PublicApiController extends Controller
'created_at', 'created_at',
'updated_at' 'updated_at'
)->where('id', $dir, $id) )->where('id', $dir, $id)
->with('profile', 'hashtags', 'mentions')
->whereIn('type', ['photo', 'photo:album', 'video', 'video:album']) ->whereIn('type', ['photo', 'photo:album', 'video', 'video:album'])
->whereLocal(true) ->whereLocal(true)
->whereNull('uri') ->whereNull('uri')
@ -300,6 +301,7 @@ class PublicApiController extends Controller
'created_at', 'created_at',
'updated_at' 'updated_at'
)->whereIn('type', ['photo', 'photo:album', 'video', 'video:album']) )->whereIn('type', ['photo', 'photo:album', 'video', 'video:album'])
->with('profile', 'hashtags', 'mentions')
->whereLocal(true) ->whereLocal(true)
->whereNull('uri') ->whereNull('uri')
->whereNotIn('profile_id', $filtered) ->whereNotIn('profile_id', $filtered)
@ -378,6 +380,7 @@ class PublicApiController extends Controller
'created_at', 'created_at',
'updated_at' 'updated_at'
)->whereIn('type', ['photo', 'photo:album', 'video', 'video:album']) )->whereIn('type', ['photo', 'photo:album', 'video', 'video:album'])
->with('profile', 'hashtags', 'mentions')
->where('id', $dir, $id) ->where('id', $dir, $id)
->whereIn('profile_id', $following) ->whereIn('profile_id', $following)
->whereNotIn('profile_id', $filtered) ->whereNotIn('profile_id', $filtered)
@ -405,6 +408,7 @@ class PublicApiController extends Controller
'created_at', 'created_at',
'updated_at' 'updated_at'
)->whereIn('type', ['photo', 'photo:album', 'video', 'video:album']) )->whereIn('type', ['photo', 'photo:album', 'video', 'video:album'])
->with('profile', 'hashtags', 'mentions')
->whereIn('profile_id', $following) ->whereIn('profile_id', $following)
->whereNotIn('profile_id', $filtered) ->whereNotIn('profile_id', $filtered)
->whereNull('in_reply_to_id') ->whereNull('in_reply_to_id')

View file

@ -52,6 +52,7 @@ class SearchController extends Controller
'entity' => [ 'entity' => [
'id' => $item->id, 'id' => $item->id,
'following' => $item->followedBy(Auth::user()->profile), 'following' => $item->followedBy(Auth::user()->profile),
'follow_request' => $item->hasFollowRequestById(Auth::user()->profile_id),
'thumb' => $item->avatarUrl() 'thumb' => $item->avatarUrl()
] ]
]]; ]];

View file

@ -1,95 +0,0 @@
<?php
namespace App\Jobs;
use App\Avatar;
use App\Profile;
use Illuminate\Bus\Queueable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
class ImportAvatar implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $url;
protected $profile;
/**
* Delete the job if its models no longer exist.
*
* @var bool
*/
public $deleteWhenMissingModels = true;
/**
* Create a new job instance.
*
* @return void
*/
public function __construct($url, Profile $profile)
{
$this->url = $url;
$this->profile = $profile;
}
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
$url = $this->url;
$profile = $this->profile;
$basePath = $this->buildPath();
}
public function buildPath()
{
$baseDir = storage_path('app/public/avatars');
if (!is_dir($baseDir)) {
mkdir($baseDir);
}
$prefix = $this->profile->id;
$padded = str_pad($prefix, 12, 0, STR_PAD_LEFT);
$parts = str_split($padded, 3);
foreach ($parts as $k => $part) {
if ($k == 0) {
$prefix = storage_path('app/public/avatars/'.$parts[0]);
if (!is_dir($prefix)) {
mkdir($prefix);
}
}
if ($k == 1) {
$prefix = storage_path('app/public/avatars/'.$parts[0].'/'.$parts[1]);
if (!is_dir($prefix)) {
mkdir($prefix);
}
}
if ($k == 2) {
$prefix = storage_path('app/public/avatars/'.$parts[0].'/'.$parts[1].'/'.$parts[2]);
if (!is_dir($prefix)) {
mkdir($prefix);
}
}
if ($k == 3) {
$avatarpath = 'public/avatars/'.$parts[0].'/'.$parts[1].'/'.$parts[2].'/'.$parts[3];
$prefix = storage_path('app/'.$avatarpath);
if (!is_dir($prefix)) {
mkdir($prefix);
}
}
}
$dir = storage_path('app/'.$avatarpath);
if (!is_dir($dir)) {
mkdir($dir);
}
$path = $avatarpath.'/avatar.svg';
return storage_path('app/'.$path);
}
}

View file

@ -34,10 +34,10 @@ class Media extends Model
$url = $this->remote_url; $url = $this->remote_url;
} else { } else {
$path = $this->media_path; $path = $this->media_path;
$url = $this->cdn_url ?? Storage::url($path); $url = $this->cdn_url ?? config('app.url') . Storage::url($path);
} }
return url($url); return $url;
} }
public function thumbnailUrl() public function thumbnailUrl()

View file

@ -140,7 +140,7 @@ class Profile extends Model
$version = hash('sha256', $avatar->change_count); $version = hash('sha256', $avatar->change_count);
$path = "{$path}?v={$version}"; $path = "{$path}?v={$version}";
return url(Storage::url($path)); return config('app.url') . Storage::url($path);
}); });
return $url; return $url;

View file

@ -15,6 +15,9 @@ class ProfileTransformer extends Fractal\TransformerAbstract
'https://w3id.org/security/v1', 'https://w3id.org/security/v1',
[ [
'manuallyApprovesFollowers' => 'as:manuallyApprovesFollowers', 'manuallyApprovesFollowers' => 'as:manuallyApprovesFollowers',
'PropertyValue' => 'schema:PropertyValue',
'schema' => 'http://schema.org#',
'value' => 'schema:value'
], ],
], ],
'id' => $profile->permalink(), 'id' => $profile->permalink(),

View file

@ -7,20 +7,20 @@ use League\Fractal;
class Announce extends Fractal\TransformerAbstract class Announce extends Fractal\TransformerAbstract
{ {
public function transform(Status $status) public function transform(Status $status)
{ {
return [ return [
'@context' => 'https://www.w3.org/ns/activitystreams', '@context' => 'https://www.w3.org/ns/activitystreams',
'id' => $status->permalink(), 'id' => $status->permalink(),
'type' => 'Announce', 'type' => 'Announce',
'actor' => $status->profile->permalink(), 'actor' => $status->profile->permalink(),
'to' => ['https://www.w3.org/ns/activitystreams#Public'], 'to' => ['https://www.w3.org/ns/activitystreams#Public'],
'cc' => [ 'cc' => [
$status->profile->permalink(), $status->profile->permalink(),
$status->profile->follower_url ?? $status->profile->permalink('/followers') $status->profile->follower_url ?? $status->profile->permalink('/followers')
], ],
'published' => $status->created_at->format(DATE_ISO8601), 'published' => $status->created_at->format(DATE_ISO8601),
'object' => $status->parent()->url(), 'object' => $status->parent()->url(),
]; ];
} }
} }

View file

@ -0,0 +1,23 @@
<?php
namespace App\Transformer\Api;
use League\Fractal;
use App\DirectMessage;
class DirectMessageTransformer extends Fractal\TransformerAbstract
{
public function transform(DirectMessage $dm)
{
return [
'id' => $dm->id,
'to_id' => $dm->to_id,
'from_id' => $dm->from_id,
'from_profile_ids' => $dm->from_profile_ids,
'group_message' => $dm->group_message,
'status_id' => $dm->status_id,
'read_at' => $dm->read_at,
'created_at' => $dm->created_at
];
}
}

View file

@ -3,7 +3,10 @@
namespace App\Transformer\Api; namespace App\Transformer\Api;
use Auth; use Auth;
use App\Profile; use App\{
FollowRequest,
Profile
};
use League\Fractal; use League\Fractal;
class RelationshipTransformer extends Fractal\TransformerAbstract class RelationshipTransformer extends Fractal\TransformerAbstract
@ -12,6 +15,12 @@ class RelationshipTransformer extends Fractal\TransformerAbstract
{ {
$auth = Auth::check(); $auth = Auth::check();
$user = $auth ? Auth::user()->profile : false; $user = $auth ? Auth::user()->profile : false;
$requested = false;
if($user) {
$requested = FollowRequest::whereFollowerId($user->id)
->whereFollowingId($profile->id)
->exists();
}
return [ return [
'id' => (string) $profile->id, 'id' => (string) $profile->id,
'following' => $auth ? $user->follows($profile) : false, 'following' => $auth ? $user->follows($profile) : false,
@ -19,7 +28,7 @@ class RelationshipTransformer extends Fractal\TransformerAbstract
'blocking' => $auth ? $user->blockedIds()->contains($profile->id) : false, 'blocking' => $auth ? $user->blockedIds()->contains($profile->id) : false,
'muting' => $auth ? $user->mutedIds()->contains($profile->id) : false, 'muting' => $auth ? $user->mutedIds()->contains($profile->id) : false,
'muting_notifications' => null, 'muting_notifications' => null,
'requested' => null, 'requested' => $requested,
'domain_blocking' => null, 'domain_blocking' => null,
'showing_reblogs' => null, 'showing_reblogs' => null,
'endorsed' => false 'endorsed' => false

View file

@ -110,14 +110,15 @@ class Image
$orientation = $ratio['orientation']; $orientation = $ratio['orientation'];
try { try {
$img = Intervention::make($file)->orientate(); $img = Intervention::make($file);
$metadata = $img->exif();
$img->orientate();
if($thumbnail) { if($thumbnail) {
$img->resize($aspect['width'], $aspect['height'], function ($constraint) { $img->resize($aspect['width'], $aspect['height'], function ($constraint) {
$constraint->aspectRatio(); $constraint->aspectRatio();
}); });
} else { } else {
if(config('media.exif.database', false) == true) { if(config('media.exif.database', false) == true) {
$metadata = $img->exif();
$media->metadata = json_encode($metadata); $media->metadata = json_encode($metadata);
} }

47
app/Util/Site/Config.php Normal file
View file

@ -0,0 +1,47 @@
<?php
namespace App\Util\Site;
use Cache;
class Config {
public static function get() {
return Cache::remember('api:site:configuration', now()->addMinutes(30), function() {
return [
'uploader' => [
'max_photo_size' => config('pixelfed.max_photo_size'),
'max_caption_length' => config('pixelfed.max_caption_length'),
'album_limit' => config('pixelfed.max_album_length'),
'image_quality' => config('pixelfed.image_quality'),
'optimize_image' => config('pixelfed.optimize_image'),
'optimize_video' => config('pixelfed.optimize_video'),
'media_types' => config('pixelfed.media_types'),
'enforce_account_limit' => config('pixelfed.enforce_account_limit')
],
'activitypub' => [
'enabled' => config('federation.activitypub.enabled'),
'remote_follow' => config('federation.activitypub.remoteFollow')
],
'ab' => [
'lc' => config('exp.lc'),
'rec' => config('exp.rec'),
'loops' => config('exp.loops')
],
'site' => [
'domain' => config('pixelfed.domain.app'),
'url' => config('app.url')
]
];
});
}
public static function json() {
return json_encode(self::get(), JSON_FORCE_OBJECT);
}
}

BIN
public/css/appdark.css vendored

Binary file not shown.

BIN
public/js/app.js vendored

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
public/js/compose.js vendored

Binary file not shown.

BIN
public/js/discover.js vendored

Binary file not shown.

BIN
public/js/profile.js vendored

Binary file not shown.

BIN
public/js/timeline.js vendored

Binary file not shown.

Binary file not shown.

View file

@ -11,5 +11,11 @@ let token = document.head.querySelector('meta[name="csrf-token"]');
if (token) { if (token) {
window.axios.defaults.headers.common['X-CSRF-TOKEN'] = token.content; window.axios.defaults.headers.common['X-CSRF-TOKEN'] = token.content;
} else { } else {
console.error('CSRF token not found: https://laravel.com/docs/csrf#csrf-x-csrf-token'); console.error('CSRF token not found.');
}
window.App = window.App || {};
window.App.boot = function() {
new Vue({ el: '#content'});
} }

View file

@ -3,7 +3,6 @@ import BootstrapVue from 'bootstrap-vue'
import InfiniteLoading from 'vue-infinite-loading'; import InfiniteLoading from 'vue-infinite-loading';
import Loading from 'vue-loading-overlay'; import Loading from 'vue-loading-overlay';
import VueTimeago from 'vue-timeago'; import VueTimeago from 'vue-timeago';
//import {Howl, Howler} from 'howler';
Vue.use(BootstrapVue); Vue.use(BootstrapVue);
Vue.use(InfiniteLoading); Vue.use(InfiniteLoading);
@ -36,126 +35,8 @@ try {
} }
window.filesize = require('filesize'); window.filesize = require('filesize');
// window.Plyr = require('plyr');
import swal from 'sweetalert'; import swal from 'sweetalert';
// require('./components/localstorage');
// require('./components/commentform');
//require('./components/searchform');
// require('./components/bookmarkform');
// require('./components/statusform');
//require('./components/embed');
//require('./components/notifications');
// import Echo from "laravel-echo"
// window.io = require('socket.io-client');
// window.pixelfed.bootEcho = function() {
// window.Echo = new Echo({
// broadcaster: 'socket.io',
// host: window.location.hostname + ':2096',
// auth: {
// headers: {
// Authorization: 'Bearer ' + token.content,
// },
// },
// });
// }
// Initialize Notification Helper
window.pixelfed.n = {};
// Vue.component(
// 'search-results',
// require('./components/SearchResults.vue').default
// );
// Vue.component(
// 'photo-presenter',
// require('./components/presenter/PhotoPresenter.vue').default
// );
// Vue.component(
// 'video-presenter',
// require('./components/presenter/VideoPresenter.vue').default
// );
// Vue.component(
// 'photo-album-presenter',
// require('./components/presenter/PhotoAlbumPresenter.vue').default
// );
// Vue.component(
// 'video-album-presenter',
// require('./components/presenter/VideoAlbumPresenter.vue').default
// );
// Vue.component(
// 'mixed-album-presenter',
// require('./components/presenter/MixedAlbumPresenter.vue').default
// );
// Vue.component(
// 'post-menu',
// require('./components/PostMenu.vue').default
// );
// Vue.component(
// 'passport-clients',
// require('./components/passport/Clients.vue').default
// );
// Vue.component(
// 'passport-authorized-clients',
// require('./components/passport/AuthorizedClients.vue').default
// );
// Vue.component(
// 'passport-personal-access-tokens',
// require('./components/passport/PersonalAccessTokens.vue').default
// );
// Vue.component(
// 'follow-suggestions',
// require('./components/FollowSuggestions.vue').default
// );
// Vue.component(
// 'circle-panel',
// require('./components/CirclePanel.vue')
// );
// Vue.component(
// 'story-compose',
// require('./components/StoryCompose.vue').default
// );
//import 'promise-polyfill/src/polyfill';
// window.pixelfed.copyToClipboard = (str) => {
// const el = document.createElement('textarea');
// el.value = str;
// el.setAttribute('readonly', '');
// el.style.position = 'absolute';
// el.style.left = '-9999px';
// document.body.appendChild(el);
// const selected =
// document.getSelection().rangeCount > 0
// ? document.getSelection().getRangeAt(0)
// : false;
// el.select();
// document.execCommand('copy');
// document.body.removeChild(el);
// if (selected) {
// document.getSelection().removeAllRanges();
// document.getSelection().addRange(selected);
// }
// };
$(document).ready(function() { $(document).ready(function() {
$(function () { $(function () {
$('[data-toggle="tooltip"]').tooltip() $('[data-toggle="tooltip"]').tooltip()

View file

@ -1,47 +1,121 @@
<template> <template>
<div> <div class="w-100 h-100">
<div class="row"> <div v-if="!loaded" style="height: 80vh;" class="d-flex justify-content-center align-items-center">
<div class="col-4 p-0 p-sm-2 p-md-3 p-xs-1" v-for="(s, index) in posts"> <img src="/img/pixelfed-icon-grey.svg" class="">
<a class="card info-overlay card-md-border-0" :href="s.url"> </div>
<div class="square"> <div class="row mt-3" v-if="loaded">
<span v-if="s.pf_type == 'photo:album'" class="float-right mr-3 post-icon"><i class="fas fa-images fa-2x"></i></span> <div class="col-12 p-0 mb-3">
<span v-if="s.pf_type == 'video'" class="float-right mr-3 post-icon"><i class="fas fa-video fa-2x"></i></span> <picture class="d-flex align-items-center justify-content-center">
<span v-if="s.pf_type == 'video:album'" class="float-right mr-3 post-icon"><i class="fas fa-film fa-2x"></i></span> <div class="dims"></div>
<div class="square-content" v-bind:style="previewBackground(s)"> <div style="z-index:500;position: absolute;" class="text-white">
</div> <p class="display-4 text-center pt-3">{{title || 'Untitled Collection'}}</p>
<div class="info-overlay-text"> <p class="lead text-center mb-3">{{description}}</p>
<h5 class="text-white m-auto font-weight-bold"> <p class="text-center">
<span> {{posts.length}} photos · by <a :href="'/' + profileUsername" class="font-weight-bold text-white">{{profileUsername}}</a>
<span class="far fa-heart fa-lg p-2 d-flex-inline"></span> </p>
<span class="d-flex-inline">{{s.favourites_count}}</span> <p v-if="owner == true" class="pt-3 text-center">
</span> <span>
<span> <button class="btn btn-outline-light btn-sm" @click.prevent="addToCollection">Add Photo</button>
<span class="fas fa-retweet fa-lg p-2 d-flex-inline"></span> &nbsp; &nbsp;
<span class="d-flex-inline">{{s.reblogs_count}}</span> <button class="btn btn-outline-light btn-sm" @click.prevent="editCollection">Edit</button>
</span> &nbsp; &nbsp;
</h5> <button class="btn btn-outline-light btn-sm" @click.prevent="deleteCollection">Delete</button>
</div> </span>
</p>
</div> </div>
</a> <img :src="previewUrl(posts[0])"
alt=""
style="width:100%; height: 600px; object-fit: cover;"
>
</picture>
</div>
<div class="col-12 p-0">
<masonry
:cols="{default: 2, 700: 2, 400: 1}"
:gutter="{default: '5px'}"
>
<div v-for="(s, index) in posts">
<a class="card info-overlay card-md-border-0 mb-1" :href="s.url">
<img :src="previewUrl(s)" class="img-fluid w-100">
</a>
</div>
</masonry>
</div> </div>
</div> </div>
<b-modal ref="editModal" id="edit-modal" hide-footer centered title="Edit Collection" body-class="">
<form>
<div class="form-group">
<label for="title" class="font-weight-bold text-muted">Title</label>
<input type="text" class="form-control" id="title" placeholder="Untitled Collection" v-model="title">
</div>
<div class="form-group">
<label for="description" class="font-weight-bold text-muted">Description</label>
<textarea class="form-control" id="description" placeholder="Add a description here ..." v-model="description" rows="3"></textarea>
</div>
<div class="form-group">
<label for="visibility" class="font-weight-bold text-muted">Visibility</label>
<select class="custom-select" v-model="visibility">
<option value="public">Public</option>
<option value="private">Followers Only</option>
</select>
</div>
<button type="button" class="btn btn-primary btn-sm py-1 font-weight-bold px-3 float-right" @click.prevent="updateCollection">Save</button>
</form>
</b-modal>
<b-modal ref="addPhotoModal" id="add-photo-modal" hide-footer centered title="Add Photo" body-class="">
<form>
<div class="form-group">
<label for="title" class="font-weight-bold text-muted">Add Post by URL</label>
<input type="text" class="form-control" placeholder="https://pixelfed.dev/p/admin/1" v-model="photoId">
<p class="help-text small text-muted">Only local, public posts can be added</p>
</div>
<button type="button" class="btn btn-primary btn-sm py-1 font-weight-bold px-3 float-right" @click.prevent="pushId">Add Photo</button>
</form>
</b-modal>
</div> </div>
</template> </template>
<style type="text/css" scoped></style> <style type="text/css" scoped>
.dims {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
background: rgba(0,0,0,.68);
z-index: 300;
}
</style>
<script type="text/javascript"> <script type="text/javascript">
import VueMasonry from 'vue-masonry-css'
Vue.use(VueMasonry);
export default { export default {
props: ['collection-id'], props: [
'collection-id',
'collection-title',
'collection-description',
'collection-visibility',
'profile-id',
'profile-username'
],
data() { data() {
return { return {
loaded: false, loaded: false,
posts: [], posts: [],
currentUser: false,
owner: false,
title: this.collectionTitle,
description: this.collectionDescription,
visibility: this.collectionVisibility,
photoId: ''
} }
}, },
beforeMount() { beforeMount() {
this.fetchCurrentUser();
this.fetchItems(); this.fetchItems();
}, },
@ -49,10 +123,19 @@ export default {
}, },
methods: { methods: {
fetchCurrentUser() {
if(document.querySelectorAll('body')[0].classList.contains('loggedIn') == true) {
axios.get('/api/v1/accounts/verify_credentials').then(res => {
this.currentUser = res.data;
this.owner = this.currentUser.id == this.profileId;
});
}
},
fetchItems() { fetchItems() {
axios.get('/api/local/collection/items/' + this.collectionId) axios.get('/api/local/collection/items/' + this.collectionId)
.then(res => { .then(res => {
this.posts = res.data; this.posts = res.data;
this.loaded = true;
}); });
}, },
@ -64,6 +147,70 @@ export default {
let preview = this.previewUrl(status); let preview = this.previewUrl(status);
return 'background-image: url(' + preview + ');'; return 'background-image: url(' + preview + ');';
}, },
addToCollection() {
this.$refs.addPhotoModal.show();
},
pushId() {
let max = 18;
if(this.posts.length >= max) {
swal('Error', 'You can only add ' + max + ' posts per collection', 'error');
return;
}
let url = this.photoId;
let origin = window.location.origin;
let split = url.split('/');
if(url.slice(0, origin.length) !== origin) {
swal('Invalid URL', 'You can only add posts from this instance', 'error');
this.photoId = '';
}
if(url.slice(0, origin.length + 3) !== origin + '/p/' || split.length !== 6) {
swal('Invalid URL', 'Invalid URL', 'error');
this.photoId = '';
}
axios.post('/api/local/collection/item', {
collection_id: this.collectionId,
post_id: split[5]
}).then(res => {
location.reload();
}).catch(err => {
swal('Invalid URL', 'The post you entered was invalid', 'error');
this.photoId = '';
});
},
editCollection() {
this.$refs.editModal.show();
},
deleteCollection() {
if(this.owner == false) {
return;
}
let confirmed = window.confirm('Are you sure you want to delete this collection?');
if(confirmed) {
axios.delete('/api/local/collection/' + this.collectionId)
.then(res => {
window.location.href = '/';
});
} else {
return;
}
},
updateCollection() {
this.$refs.editModal.hide();
axios.post('/api/local/collection/' + this.collectionId, {
title: this.title,
description: this.description,
visibility: this.visibility
}).then(res => {
console.log(res.data);
});
}
} }
} }
</script> </script>

View file

@ -37,7 +37,12 @@
<p class="text-center mb-0 font-weight-bold text-white"><i class="fas fa-plus mr-1"></i> Add Photo</p> <p class="text-center mb-0 font-weight-bold text-white"><i class="fas fa-plus mr-1"></i> Add Photo</p>
</div> </div>
<div v-if="ids.length == 0" class="w-100 h-100 bg-light py-5 cursor-pointer" style="border-bottom: 1px solid #f1f1f1" v-on:click="addMedia($event)"> <div v-if="ids.length == 0" class="w-100 h-100 bg-light py-5 cursor-pointer" style="border-bottom: 1px solid #f1f1f1" v-on:click="addMedia($event)">
<p class="text-center mb-0 font-weight-bold p-5">{{composeMessage()}}</p> <div class="p-5">
<p class="text-center font-weight-bold">{{composeMessage()}}</p>
<p class="text-muted mb-0 small text-center">Accepted Formats: <b>{{acceptedFormats()}}</b></p>
<p class="text-muted mb-0 small text-center">Max File Size: <b>{{maxSize()}}</b></p>
<p class="text-muted mb-0 small text-center">Albums can contain up to <b>{{config.uploader.album_limit}}</b> photos or videos</p>
</div>
</div> </div>
<div v-if="ids.length > 0"> <div v-if="ids.length > 0">
@ -173,19 +178,20 @@
{{composeText.length}} / {{config.uploader.max_caption_length}} {{composeText.length}} / {{config.uploader.max_caption_length}}
</div> </div>
<div class="pl-md-5"> <div class="pl-md-5">
<div class="btn-group"> <!-- <div class="btn-group">
<button type="button" class="btn btn-primary btn-sm font-weight-bold" v-on:click="compose()">{{composeState[0].toUpperCase() + composeState.slice(1)}}</button> <button type="button" class="btn btn-primary btn-sm font-weight-bold" v-on:click="compose()">{{composeState[0].toUpperCase() + composeState.slice(1)}}</button>
<button type="button" class="btn btn-primary btn-sm dropdown-toggle dropdown-toggle-split" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> <button type="button" class="btn btn-primary btn-sm dropdown-toggle dropdown-toggle-split" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="sr-only">Toggle Dropdown</span> <span class="sr-only">Toggle Dropdown</span>
</button> </button>
<div class="dropdown-menu dropdown-menu-right"> <div class="dropdown-menu dropdown-menu-right">
<a :class="[composeState == 'publish' ?'dropdown-item font-weight-bold active':'dropdown-item font-weight-bold ']" href="#" v-on:click.prevent="composeState = 'publish'">Publish now</a> <a :class="[composeState == 'publish' ?'dropdown-item font-weight-bold active':'dropdown-item font-weight-bold ']" href="#" v-on:click.prevent="composeState = 'publish'">Publish now</a>
<!-- <a :class="[composeState == 'draft' ?'dropdown-item font-weight-bold active':'dropdown-item font-weight-bold ']" href="#" v-on:click.prevent="composeState = 'draft'">Save as draft</a> <!- - <a :class="[composeState == 'draft' ?'dropdown-item font-weight-bold active':'dropdown-item font-weight-bold ']" href="#" v-on:click.prevent="composeState = 'draft'">Save as draft</a>
<a :class="[composeState == 'schedule' ?'dropdown-item font-weight-bold active':'dropdown-item font-weight-bold ']" href="#" v-on:click.prevent="composeState = 'schedule'">Schedule for later</a> <a :class="[composeState == 'schedule' ?'dropdown-item font-weight-bold active':'dropdown-item font-weight-bold ']" href="#" v-on:click.prevent="composeState = 'schedule'">Schedule for later</a>
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
<a :class="[composeState == 'delete' ?'dropdown-item font-weight-bold active':'dropdown-item font-weight-bold ']" href="#" v-on:click.prevent="composeState = 'delete'">Delete</a> --> <a :class="[composeState == 'delete' ?'dropdown-item font-weight-bold active':'dropdown-item font-weight-bold ']" href="#" v-on:click.prevent="composeState = 'delete'">Delete</a> - ->
</div> </div>
</div> </div> -->
<button class="btn btn-primary btn-sm font-weight-bold px-3" v-on:click="compose()">Publish</button>
</div> </div>
</div> </div>
</div> </div>
@ -218,11 +224,7 @@
export default { export default {
data() { data() {
return { return {
config: { config: window.App.config,
uploader: {
media_types: '',
}
},
profile: {}, profile: {},
composeText: '', composeText: '',
composeTextLength: 0, composeTextLength: 0,
@ -241,7 +243,6 @@ export default {
}, },
beforeMount() { beforeMount() {
this.fetchConfig();
this.fetchProfile(); this.fetchProfile();
}, },
@ -290,20 +291,9 @@ export default {
['Willow','filter-willow'], ['Willow','filter-willow'],
['X-Pro II','filter-xpro-ii'] ['X-Pro II','filter-xpro-ii']
]; ];
}, },
methods: { methods: {
fetchConfig() {
axios.get('/api/v2/config').then(res => {
this.config = res.data;
if(this.config.uploader.media_types.includes('video/mp4') == false) {
this.composeType = 'post'
}
});
},
fetchProfile() { fetchProfile() {
axios.get('/api/v1/accounts/verify_credentials').then(res => { axios.get('/api/v1/accounts/verify_credentials').then(res => {
this.profile = res.data; this.profile = res.data;
@ -311,7 +301,6 @@ export default {
this.visibility = 'private'; this.visibility = 'private';
} }
}).catch(err => { }).catch(err => {
console.log(err)
}); });
}, },
@ -464,6 +453,11 @@ export default {
let data = res.data; let data = res.data;
window.location.href = data; window.location.href = data;
}).catch(err => { }).catch(err => {
let res = err.response.data;
if(res.message == 'Too Many Attempts.') {
swal('You\'re posting too much!', 'We only allow 50 posts per hour or 100 per day. If you\'ve reached that limit, please try again later. If you think this is an error, please contact an administrator.', 'error');
return;
}
swal('Oops, something went wrong!', 'An unexpected error occurred.', 'error'); swal('Oops, something went wrong!', 'An unexpected error occurred.', 'error');
}); });
return; return;
@ -511,6 +505,18 @@ export default {
createCollection() { createCollection() {
window.location.href = '/i/collections/create'; window.location.href = '/i/collections/create';
},
maxSize() {
let limit = this.config.uploader.max_photo_size;
return limit / 1000 + ' MB';
},
acceptedFormats() {
let formats = this.config.uploader.media_types;
return formats.split(',').map(f => {
return ' ' + f.split('/')[1];
}).toString();
} }
} }
} }

View file

@ -1,122 +1,105 @@
<template> <template>
<div class="container"> <div>
<div v-if="!loaded" style="height: 70vh;" class="d-flex justify-content-center align-items-center">
<img src="/img/pixelfed-icon-grey.svg">
</div>
<div v-else>
<div class="d-block d-md-none px-0 border-top-0 mx-n3">
<input class="form-control rounded-0" placeholder="Search" v-model="searchTerm" v-on:keyup.enter="searchSubmit">
</div>
<section class="d-none d-md-flex mb-md-2 pt-5 discover-bar" style="width:auto; overflow: auto hidden;" v-if="categories.length > 0">
<a v-if="config.ab.loops == true" class="text-decoration-none bg-transparent border border-success rounded d-inline-flex align-items-center justify-content-center mr-3 card-disc" href="/discover/loops">
<p class="text-success lead font-weight-bold mb-0">Loops</p>
</a>
<a v-for="(category, index) in categories" :key="index+'_cat_'" class="bg-dark rounded d-inline-flex align-items-end justify-content-center mr-3 box-shadow card-disc" :href="category.url" :style="'background: linear-gradient(rgba(0, 0, 0, 0.3),rgba(0, 0, 0, 0.3)),url('+category.thumb+');'">
<p class="text-white font-weight-bold" style="text-shadow: 3px 3px 16px #272634;">{{category.name}}</p>
</a>
<section class="d-none d-md-flex mb-md-2 pt-2 discover-bar" style="width:auto; overflow: auto hidden;" v-if="categories.length > 0"> </section>
<a v-if="config.ab.loops == true" class="text-decoration-none bg-transparent border border-success rounded d-inline-flex align-items-center justify-content-center mr-3 card-disc" href="/discover/loops"> <section class="mb-5 section-explore">
<p class="text-success lead font-weight-bold mb-0">Loops</p> <div class="profile-timeline">
</a> <div class="row p-0">
<!-- <a class="text-decoration-none rounded d-inline-flex align-items-center justify-content-center mr-3 box-shadow card-disc" href="/discover/personal" style="background: rgb(255, 95, 109);"> <div class="col-4 p-1 p-sm-2 p-md-3" v-for="post in posts">
<p class="text-white lead font-weight-bold mb-0">For You</p> <a class="card info-overlay card-md-border-0" :href="post.url">
</a> --> <div class="square">
<span v-if="post.type == 'photo:album'" class="float-right mr-3 post-icon"><i class="fas fa-images fa-2x"></i></span>
<a v-for="(category, index) in categories" :key="index+'_cat_'" class="bg-dark rounded d-inline-flex align-items-end justify-content-center mr-3 box-shadow card-disc" :href="category.url" :style="'background: linear-gradient(rgba(0, 0, 0, 0.3),rgba(0, 0, 0, 0.3)),url('+category.thumb+');'"> <span v-if="post.type == 'video'" class="float-right mr-3 post-icon"><i class="fas fa-video fa-2x"></i></span>
<p class="text-white font-weight-bold" style="text-shadow: 3px 3px 16px #272634;">{{category.name}}</p> <span v-if="post.type == 'video:album'" class="float-right mr-3 post-icon"><i class="fas fa-film fa-2x"></i></span>
</a> <div class="square-content" v-bind:style="{ 'background-image': 'url(' + post.thumb + ')' }">
</div>
</section> </div>
<section class="mb-5 section-explore"> </a>
<div class="profile-timeline"> </div>
<div class="loader text-center"> </div>
<div class="lds-ring"><div></div><div></div><div></div><div></div></div> </div>
</div> </section>
<div class="row d-none"> <section class="mb-5">
<div class="col-4 p-0 p-sm-2 p-md-3" v-for="post in posts"> <p class="lead text-center">To view more posts, check the <a href="/" class="font-weight-bold">home</a> or <a href="/timeline/public" class="font-weight-bold">local</a> timelines.</p>
<a class="card info-overlay card-md-border-0" :href="post.url"> </section>
<div class="square"> </div>
<span v-if="post.type == 'photo:album'" class="float-right mr-3 post-icon"><i class="fas fa-images fa-2x"></i></span> </div>
<span v-if="post.type == 'video'" class="float-right mr-3 post-icon"><i class="fas fa-video fa-2x"></i></span>
<span v-if="post.type == 'video:album'" class="float-right mr-3 post-icon"><i class="fas fa-film fa-2x"></i></span>
<div class="square-content" v-bind:style="{ 'background-image': 'url(' + post.thumb + ')' }">
</div>
</div>
</a>
</div>
</div>
</div>
</section>
<section class="mb-5">
<p class="lead text-center">To view more posts, check the <a href="/" class="font-weight-bold">home</a> or <a href="/timeline/public" class="font-weight-bold">local</a> timelines.</p>
</section>
</div>
</template> </template>
<style type="text/css" scoped> <style type="text/css" scoped>
.discover-bar::-webkit-scrollbar { .discover-bar::-webkit-scrollbar {
display: none; display: none;
} }
.card-disc { .card-disc {
flex: 0 0 160px; flex: 0 0 160px;
width:160px; width:160px;
height:100px; height:100px;
background-size: cover !important; background-size: cover !important;
} }
.post-icon { .post-icon {
color: #fff; color: #fff;
position:relative; position:relative;
margin-top: 10px; margin-top: 10px;
z-index: 9; z-index: 9;
opacity: 0.6; opacity: 0.6;
text-shadow: 3px 3px 16px #272634; text-shadow: 3px 3px 16px #272634;
} }
</style> </style>
<script type="text/javascript"> <script type="text/javascript">
export default { export default {
data() { data() {
return { return {
config: {}, loaded: false,
posts: {}, config: window.App.config,
trending: {}, posts: {},
categories: {}, trending: {},
allCategories: {}, categories: {},
} allCategories: {},
}, searchTerm: '',
mounted() { }
this.fetchData(); },
this.fetchCategories(); mounted() {
}, this.fetchData();
this.fetchCategories();
methods: {
followUser(id, event) {
axios.post('/i/follow', {
item: id
}).then(res => {
let el = $(event.target);
el.addClass('btn-outline-secondary').removeClass('btn-primary');
el.text('Unfollow');
}).catch(err => {
if(err.response.data.message) {
swal('Error', err.response.data.message, 'error');
}
});
},
fetchData() {
axios.get('/api/v2/config')
.then((res) => {
let data = res.data;
this.config = data;
});
axios.get('/api/v2/discover/posts')
.then((res) => {
let data = res.data;
this.posts = data.posts;
if(this.posts.length > 1) {
$('.section-explore .loader').hide();
$('.section-explore .row.d-none').removeClass('d-none');
}
});
}, },
fetchCategories() { methods: {
axios.get('/api/v2/discover/categories') fetchData() {
.then(res => { axios.get('/api/v2/discover/posts')
this.allCategories = res.data; .then((res) => {
this.categories = res.data; this.posts = res.data.posts;
}); this.loaded = true;
}, });
},
fetchCategories() {
axios.get('/api/v2/discover/categories')
.then(res => {
this.allCategories = res.data;
this.categories = res.data;
});
},
searchSubmit() {
if(this.searchTerm.length > 1) {
window.location.href = '/i/results?q=' + this.searchTerm;
}
}
}
} }
}
</script> </script>

View file

@ -1,5 +1,19 @@
<template> <template>
<div class="w-100 h-100"> <div class="w-100 h-100">
<div v-if="isMobile" class="bg-white p-3 border-bottom">
<div class="d-flex justify-content-between align-items-center">
<div @click="goBack" class="cursor-pointer">
<i class="fas fa-chevron-left fa-lg"></i>
</div>
<div class="font-weight-bold">
{{this.profileUsername}}
</div>
<div>
<a class="fas fa-ellipsis-v fa-lg text-muted text-decoration-none" href="#" @click.prevent="visitorMenu"></a>
</div>
</div>
</div>
<div v-if="relationship && relationship.blocking && warning" class="bg-white pt-3 border-bottom"> <div v-if="relationship && relationship.blocking && warning" class="bg-white pt-3 border-bottom">
<div class="container"> <div class="container">
<p class="text-center font-weight-bold">You are blocking this account</p> <p class="text-center font-weight-bold">You are blocking this account</p>
@ -10,46 +24,55 @@
<img src="/img/pixelfed-icon-grey.svg" class=""> <img src="/img/pixelfed-icon-grey.svg" class="">
</div> </div>
<div v-if="!loading && !warning"> <div v-if="!loading && !warning">
<div v-if="profileLayout == 'metro'"> <div v-if="layout == 'metro'" class="container">
<div class="bg-white py-5 border-bottom"> <div :class="isMobile ? 'pt-5' : 'pt-5 border-bottom'">
<div class="container"> <div class="container px-0">
<div class="row"> <div class="row">
<div class="col-12 col-md-4 d-flex"> <div class="col-12 col-md-4 d-md-flex">
<div class="profile-avatar mx-md-auto"> <div class="profile-avatar mx-md-auto">
<div class="d-block d-md-none">
<div class="row">
<div class="col-5">
<img class="rounded-circle box-shadow mr-2" :src="profile.avatar" width="77px" height="77px">
<p v-if="sponsorList.patreon || sponsorList.liberapay || sponsorList.opencollective" class="text-center mt-1 mr-2">
<button type="button" @click="showSponsorModal" class="btn btn-sm btn-outline-secondary font-weight-bold py-0">
Donate
</button>
</p>
</div>
<div class="col-7 pl-2">
<p class="align-middle">
<span class="font-weight-ultralight h3 mb-0">{{profile.username}}</span> <!-- MOBILE PROFILE PICTURE -->
<span class="float-right mb-0" v-if="!loading && profile.id != user.id && user.hasOwnProperty('id')"> <div class="d-block d-md-none mt-n3 mb-3">
<a class="fas fa-cog fa-lg text-muted text-decoration-none" href="#" @click.prevent="visitorMenu"></a> <div class="row">
</span> <div class="col-4">
</p> <img :alt="profileUsername + '\'s profile picture'" class="rounded-circle border mr-2" :src="profile.avatar" width="77px" height="77px">
<p v-if="!loading && profile.id == user.id && user.hasOwnProperty('id')"> </div>
<a class="btn btn-outline-dark py-0 px-4 mt-3" href="/settings/home">Edit Profile</a> <div class="col-8">
</p> <div class="d-block d-md-none mt-3 py-2">
<div v-if="profile.id != user.id && user.hasOwnProperty('id')"> <ul class="nav d-flex justify-content-between">
<p class="mt-3 mb-0" v-if="relationship.following == true"> <li class="nav-item">
<button type="button" class="btn btn-outline-dark font-weight-bold px-4 py-0" v-on:click="followProfile()" data-toggle="tooltip" title="Unfollow">Unfollow</button> <div class="font-weight-light">
</p> <span class="text-dark text-center">
<p class="mt-3 mb-0" v-if="!relationship.following"> <p class="font-weight-bold mb-0">{{profile.statuses_count}}</p>
<button type="button" class="btn btn-outline-dark font-weight-bold px-4 py-0" v-on:click="followProfile()" data-toggle="tooltip" title="Follow">Follow</button> <p class="text-muted mb-0 small">Posts</p>
</p> </span>
</div>
</li>
<li class="nav-item">
<div v-if="profileSettings.followers.count" class="font-weight-light">
<a class="text-dark cursor-pointer text-center" v-on:click="followersModal()">
<p class="font-weight-bold mb-0">{{profile.followers_count}}</p>
<p class="text-muted mb-0 small">Followers</p>
</a>
</div>
</li>
<li class="nav-item">
<div v-if="profileSettings.following.count" class="font-weight-light">
<a class="text-dark cursor-pointer text-center" v-on:click="followingModal()">
<p class="font-weight-bold mb-0">{{profile.following_count}}</p>
<p class="text-muted mb-0 small">Following</p>
</a>
</div>
</li>
</ul>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="d-none d-md-block">
<img class="rounded-circle box-shadow" :src="profile.avatar" width="172px" height="172px"> <!-- DESKTOP PROFILE PICTURE -->
<div class="d-none d-md-block pb-5">
<img :alt="profileUsername + '\'s profile picture'" class="rounded-circle box-shadow" :src="profile.avatar" width="150px" height="150px">
<p v-if="sponsorList.patreon || sponsorList.liberapay || sponsorList.opencollective" class="text-center mt-3"> <p v-if="sponsorList.patreon || sponsorList.liberapay || sponsorList.opencollective" class="text-center mt-3">
<button type="button" @click="showSponsorModal" class="btn btn-outline-secondary font-weight-bold py-0"> <button type="button" @click="showSponsorModal" class="btn btn-outline-secondary font-weight-bold py-0">
<i class="fas fa-heart text-danger"></i> <i class="fas fa-heart text-danger"></i>
@ -61,110 +84,87 @@
</div> </div>
<div class="col-12 col-md-8 d-flex align-items-center"> <div class="col-12 col-md-8 d-flex align-items-center">
<div class="profile-details"> <div class="profile-details">
<div class="d-none d-md-flex username-bar pb-2 align-items-center"> <div class="d-none d-md-flex username-bar pb-3 align-items-center">
<span class="font-weight-ultralight h3">{{profile.username}}</span> <span class="font-weight-ultralight h3 mb-0">{{profile.username}}</span>
<span class="pl-4" v-if="profile.is_admin"> <span class="pl-1 pb-2" v-if="profile.is_admin" title="Admin Account" data-toggle="tooltip">
<span class="btn btn-outline-secondary font-weight-bold py-0">ADMIN</span> <i class="fas fa-certificate fa-lg text-primary">
</span> </i>
<span class="pl-4"> <i class="fas fa-check text-white fa-sm" style="font-size:9px;margin-left: -1.1rem;padding-bottom: 0.6rem;"></i>
<a :href="'/users/'+profile.username+'.atom'" class="fas fa-rss fa-lg text-muted text-decoration-none"></a>
</span>
<span class="pl-4" v-if="owner && user.hasOwnProperty('id')">
<a class="fas fa-cog fa-lg text-muted text-decoration-none" href="/settings/home"></a>
</span>
<span class="pl-4" v-if="!owner && user.hasOwnProperty('id')">
<a class="fas fa-cog fa-lg text-muted text-decoration-none" href="#" @click.prevent="visitorMenu"></a>
</span> </span>
<span v-if="profile.id != user.id && user.hasOwnProperty('id')"> <span v-if="profile.id != user.id && user.hasOwnProperty('id')">
<span class="pl-4" v-if="relationship.following == true"> <span class="pl-4" v-if="relationship.following == true">
<button type="button" class="btn btn-outline-secondary font-weight-bold btn-sm" v-on:click="followProfile()" data-toggle="tooltip" title="Unfollow"><i class="fas fa-user-minus"></i></button> <button type="button" class="btn btn-outline-secondary font-weight-bold btn-sm py-1" v-on:click="followProfile" data-toggle="tooltip" title="Unfollow">FOLLOWING</button>
</span> </span>
<span class="pl-4" v-if="!relationship.following"> <span class="pl-4" v-if="!relationship.following">
<button type="button" class="btn btn-primary font-weight-bold btn-sm" v-on:click="followProfile()" data-toggle="tooltip" title="Follow"><i class="fas fa-user-plus"></i></button> <button type="button" class="btn btn-primary font-weight-bold btn-sm py-1" v-on:click="followProfile" data-toggle="tooltip" title="Follow">FOLLOW</button>
</span> </span>
</span> </span>
<span class="pl-4" v-if="owner && user.hasOwnProperty('id')">
<a class="btn btn-outline-secondary btn-sm" href="/settings/home" style="font-weight: 600;">Edit Profile</a>
</span>
<span class="pl-4" v-else>
<a class="fas fa-ellipsis-h fa-lg text-muted text-decoration-none" href="#" @click.prevent="visitorMenu"></a>
</span>
</div> </div>
<div class="d-none d-md-inline-flex profile-stats pb-3 lead"> <div class="font-size-16px">
<div class="font-weight-light pr-5"> <div class="d-none d-md-inline-flex profile-stats pb-3">
<a class="text-dark" :href="profile.url"> <div class="font-weight-light pr-5">
<span class="font-weight-bold">{{profile.statuses_count}}</span> <span class="text-dark">
Posts <span class="font-weight-bold">{{profile.statuses_count}}</span>
</a> Posts
</div> </span>
<div v-if="profileSettings.followers.count" class="font-weight-light pr-5"> </div>
<a class="text-dark cursor-pointer" v-on:click="followersModal()"> <div v-if="profileSettings.followers.count" class="font-weight-light pr-5">
<span class="font-weight-bold">{{profile.followers_count}}</span> <a class="text-dark cursor-pointer" v-on:click="followersModal()">
Followers <span class="font-weight-bold">{{profile.followers_count}}</span>
</a> Followers
</div> </a>
<div v-if="profileSettings.following.count" class="font-weight-light"> </div>
<a class="text-dark cursor-pointer" v-on:click="followingModal()"> <div v-if="profileSettings.following.count" class="font-weight-light">
<span class="font-weight-bold">{{profile.following_count}}</span> <a class="text-dark cursor-pointer" v-on:click="followingModal()">
Following <span class="font-weight-bold">{{profile.following_count}}</span>
</a> Following
</a>
</div>
</div> </div>
<p class="mb-0 d-flex align-items-center">
<span class="font-weight-bold pr-3">{{profile.display_name}}</span>
</p>
<div v-if="profile.note" class="mb-0" v-html="profile.note"></div>
<p v-if="profile.website" class=""><a :href="profile.website" class="profile-website" rel="me external nofollow noopener" target="_blank">{{profile.website}}</a></p>
</div> </div>
<p class="lead mb-0 d-flex align-items-center pt-3">
<span class="font-weight-bold pr-3">{{profile.display_name}}</span>
</p>
<div v-if="profile.note" class="mb-0 lead" v-html="profile.note"></div>
<p v-if="profile.website" class=""><a :href="profile.website" class="font-weight-bold" rel="me external nofollow noopener" target="_blank">{{profile.website}}</a></p>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="d-block d-md-none bg-white my-0 py-2 border-bottom"> <div class="d-block d-md-none my-0 pt-3 border-bottom">
<ul class="nav d-flex justify-content-center"> <p v-if="user && user.hasOwnProperty('id')" class="pt-3">
<li class="nav-item"> <button v-if="owner" class="btn btn-outline-secondary bg-white btn-sm py-1 btn-block text-center font-weight-bold text-dark border border-lighter" @click.prevent="redirect('/settings/home')">Edit Profile</button>
<div class="font-weight-light"> <button v-if="!owner && relationship.following" class="btn btn-outline-secondary bg-white btn-sm py-1 px-5 font-weight-bold text-dark border border-lighter" @click="followProfile">&nbsp;&nbsp; Unfollow &nbsp;&nbsp;</button>
<span class="text-dark text-center"> <button v-if="!owner && !relationship.following" class="btn btn-primary btn-sm py-1 px-5 font-weight-bold" @click="followProfile">{{relationship.followed_by ? 'Follow Back' : '&nbsp;&nbsp;&nbsp;&nbsp; Follow &nbsp;&nbsp;&nbsp;&nbsp;'}}</button>
<p class="font-weight-bold mb-0">{{profile.statuses_count}}</p> <!-- <button v-if="!owner" class="btn btn-outline-secondary bg-white btn-sm py-1 px-5 font-weight-bold text-dark border border-lighter mx-2">Message</button>
<p class="text-muted mb-0">Posts</p> <button v-if="!owner" class="btn btn-outline-secondary bg-white btn-sm py-1 font-weight-bold text-dark border border-lighter"><i class="fas fa-chevron-down fa-sm"></i></button> -->
</span> </p>
</div>
</li>
<li class="nav-item px-5">
<div v-if="profileSettings.followers.count" class="font-weight-light">
<a class="text-dark cursor-pointer text-center" v-on:click="followersModal()">
<p class="font-weight-bold mb-0">{{profile.followers_count}}</p>
<p class="text-muted mb-0">Followers</p>
</a>
</div>
</li>
<li class="nav-item">
<div v-if="profileSettings.following.count" class="font-weight-light">
<a class="text-dark cursor-pointer text-center" v-on:click="followingModal()">
<p class="font-weight-bold mb-0">{{profile.following_count}}</p>
<p class="text-muted mb-0">Following</p>
</a>
</div>
</li>
</ul>
</div> </div>
<div class="bg-white"> <div class="">
<ul class="nav nav-topbar d-flex justify-content-center border-0"> <ul class="nav nav-topbar d-flex justify-content-center border-0">
<li class="nav-item border-top">
<li class="nav-item"> <a :class="this.mode == 'grid' ? 'nav-link text-dark' : 'nav-link'" href="#" v-on:click.prevent="switchMode('grid')"><i class="fas fa-th"></i> <span class="d-none d-md-inline-block small pl-1">POSTS</span></a>
<a :class="this.mode == 'grid' ? 'nav-link font-weight-bold text-uppercase text-primary' : 'nav-link font-weight-bold text-uppercase'" href="#" v-on:click.prevent="switchMode('grid')"><i class="fas fa-th fa-lg"></i></a>
</li> </li>
<li class="nav-item px-0 border-top">
<li class="nav-item px-3"> <a :class="this.mode == 'collections' ? 'nav-link text-dark' : 'nav-link'" href="#" v-on:click.prevent="switchMode('collections')"><i class="fas fa-images"></i> <span class="d-none d-md-inline-block small pl-1">COLLECTIONS</span></a>
<a :class="this.mode == 'list' ? 'nav-link font-weight-bold text-uppercase text-primary' : 'nav-link font-weight-bold text-uppercase'" href="#" v-on:click.prevent="switchMode('list')"><i class="fas fa-th-list fa-lg"></i></a>
</li> </li>
<li class="nav-item pr-3"> <li v-if="owner" class="nav-item border-top">
<a :class="this.mode == 'collections' ? 'nav-link font-weight-bold text-uppercase text-primary' : 'nav-link font-weight-bold text-uppercase'" href="#" v-on:click.prevent="switchMode('collections')"><i class="fas fa-images fa-lg"></i></a> <a :class="this.mode == 'bookmarks' ? 'nav-link text-dark' : 'nav-link'" href="#" v-on:click.prevent="switchMode('bookmarks')"><i class="fas fa-bookmark"></i></a>
</li>
<li class="nav-item" v-if="owner">
<a :class="this.mode == 'bookmarks' ? 'nav-link font-weight-bold text-uppercase text-primary' : 'nav-link font-weight-bold text-uppercase'" href="#" v-on:click.prevent="switchMode('bookmarks')"><i class="fas fa-bookmark fa-lg"></i></a>
</li> </li>
</ul> </ul>
</div> </div>
<div class="container"> <div class="container px-0">
<div class="profile-timeline mt-md-4"> <div class="profile-timeline mt-md-4">
<div class="row" v-if="mode == 'grid'"> <div class="row" v-if="mode == 'grid'">
<div class="col-4 p-0 p-sm-2 p-md-3 p-xs-1" v-for="(s, index) in timeline"> <div class="col-4 p-1 p-md-3" v-for="(s, index) in timeline">
<a class="card info-overlay card-md-border-0" :href="s.url"> <a class="card info-overlay card-md-border-0" :href="s.url">
<div class="square"> <div class="square">
<span v-if="s.pf_type == 'photo:album'" class="float-right mr-3 post-icon"><i class="fas fa-images fa-2x"></i></span> <span v-if="s.pf_type == 'photo:album'" class="float-right mr-3 post-icon"><i class="fas fa-images fa-2x"></i></span>
@ -188,115 +188,21 @@
</a> </a>
</div> </div>
</div> </div>
<div class="row" v-if="mode == 'list'"> <div v-if="mode == 'grid' != -1 && timeline.length == 0">
<div class="col-md-8 col-lg-8 offset-md-2 px-0 timeline">
<div class="card status-card card-md-rounded-0 my-sm-2 my-md-3 my-lg-4" :data-status-id="status.id" v-for="(status, index) in timeline" :key="status.id">
<div class="card-header d-inline-flex align-items-center bg-white">
<img v-bind:src="status.account.avatar" width="32px" height="32px" style="border-radius: 32px;">
<a class="username font-weight-bold pl-2 text-dark" v-bind:href="status.account.url">
{{status.account.username}}
</a>
<div v-if="user.hasOwnProperty('id')" class="text-right" style="flex-grow:1;">
<div 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 fa-lg text-muted"></span>
</button>
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownMenuButton">
<a class="dropdown-item font-weight-bold" :href="status.url">Go to post</a>
<span v-if="status.account.id != user.id">
<a class="dropdown-item font-weight-bold" :href="reportUrl(status)">Report</a>
<a class="dropdown-item font-weight-bold" v-on:click="muteProfile(status)">Mute Profile</a>
<a class="dropdown-item font-weight-bold" v-on:click="blockProfile(status)">Block Profile</a>
</span>
<span v-if="status.account.id == user.id || user.is_admin == true">
<a class="dropdown-item font-weight-bold" :href="editUrl(status)">Edit</a>
<a class="dropdown-item font-weight-bold text-danger" v-on:click="deletePost(status)">Delete</a>
</span>
</div>
</div>
</div>
</div>
<div class="postPresenterContainer">
<div v-if="status.pf_type === 'photo'" class="w-100">
<photo-presenter :status="status"></photo-presenter>
</div>
<div v-else-if="status.pf_type === 'video'" class="w-100">
<video-presenter :status="status"></video-presenter>
</div>
<div v-else-if="status.pf_type === 'photo:album'" class="w-100">
<photo-album-presenter :status="status"></photo-album-presenter>
</div>
<div v-else-if="status.pf_type === 'video:album'" class="w-100">
<video-album-presenter :status="status"></video-album-presenter>
</div>
<div v-else-if="status.pf_type === 'photo:video:album'" class="w-100">
<mixed-album-presenter :status="status"></mixed-album-presenter>
</div>
<div v-else class="w-100">
<p class="text-center p-0 font-weight-bold text-white">Error: Problem rendering preview.</p>
</div>
</div>
<div class="card-body">
<div class="reactions my-1" v-if="user.hasOwnProperty('id')">
<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 cursor-pointer']" title="Like" v-on:click="likeStatus(status, $event)"></h3>
<h3 class="far fa-comment 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 ? '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(status, $event)"></h3>
</div>
<div class="likes font-weight-bold">
<span class="like-count">{{status.favourites_count}}</span> {{status.favourites_count == 1 ? 'like' : 'likes'}}
</div>
<div class="caption">
<p class="mb-2 read-more" style="overflow: hidden;">
<span class="username font-weight-bold">
<bdi><a class="text-dark" :href="status.account.url">{{status.account.username}}</a></bdi>
</span>
<span v-html="status.content"></span>
</p>
</div>
<div class="comments">
</div>
<div class="timestamp pt-1">
<p class="small text-uppercase mb-0">
<a :href="status.url" class="text-muted">
<timeago :datetime="status.created_at" :auto-update="60" :converter-options="{includeSeconds:true}" :title="timestampFormat(status.created_at)" v-b-tooltip.hover.bottom></timeago>
</a>
</p>
</div>
</div>
<div class="card-footer bg-white d-none">
<form class="" v-on:submit.prevent="commentSubmit(status, $event)">
<input type="hidden" name="item" value="">
<input class="form-control status-reply-input" name="comment" placeholder="Add a comment…" autocomplete="off">
</form>
</div>
</div>
</div>
</div>
<div v-if="['grid','list'].indexOf(mode) != -1 && timeline.length == 0">
<div class="py-5 text-center text-muted"> <div class="py-5 text-center text-muted">
<p><i class="fas fa-camera-retro fa-2x"></i></p> <p><i class="fas fa-camera-retro fa-2x"></i></p>
<p class="h2 font-weight-light pt-3">No posts yet</p> <p class="h2 font-weight-light pt-3">No posts yet</p>
</div> </div>
</div> </div>
<div v-if="timeline.length && ['grid','list'].indexOf(mode) != -1"> <div v-if="timeline.length && mode == 'grid'">
<infinite-loading @infinite="infiniteTimeline"> <infinite-loading @infinite="infiniteTimeline">
<div slot="no-more"></div> <div slot="no-more"></div>
<div slot="no-results"></div> <div slot="no-results"></div>
</infinite-loading> </infinite-loading>
</div> </div>
<div class="row" v-if="mode == 'bookmarks'"> <div v-if="mode == 'bookmarks'">
<div v-if="bookmarks.length"> <div v-if="bookmarks.length" class="row">
<div class="col-4 p-0 p-sm-2 p-md-3 p-xs-1" v-for="(s, index) in bookmarks"> <div class="col-4 p-1 p-sm-2 p-md-3" v-for="(s, index) in bookmarks">
<a class="card info-overlay card-md-border-0" :href="s.url"> <a class="card info-overlay card-md-border-0" :href="s.url">
<div class="square"> <div class="square">
<span v-if="s.pf_type == 'photo:album'" class="float-right mr-3 post-icon"><i class="fas fa-images fa-2x"></i></span> <span v-if="s.pf_type == 'photo:album'" class="float-right mr-3 post-icon"><i class="fas fa-images fa-2x"></i></span>
@ -323,13 +229,13 @@
<div v-else class="col-12"> <div v-else class="col-12">
<div class="py-5 text-center text-muted"> <div class="py-5 text-center text-muted">
<p><i class="fas fa-bookmark fa-2x"></i></p> <p><i class="fas fa-bookmark fa-2x"></i></p>
<p class="h2 font-weight-light pt-3">You have no saved bookmarks</p> <p class="h2 font-weight-light pt-3">No saved bookmarks</p>
</div> </div>
</div> </div>
</div> </div>
<div class="col-12" v-if="mode == 'collections'"> <div v-if="mode == 'collections'">
<div v-if="collections.length" class="row"> <div v-if="collections.length" class="row">
<div class="col-4 p-0 p-sm-2 p-md-3 p-xs-1" v-for="(c, index) in collections"> <div class="col-4 p-1 p-sm-2 p-md-3" v-for="(c, index) in collections">
<a class="card info-overlay card-md-border-0" :href="c.url"> <a class="card info-overlay card-md-border-0" :href="c.url">
<div class="square"> <div class="square">
<div class="square-content" v-bind:style="'background-image: url(' + c.thumb + ');'"> <div class="square-content" v-bind:style="'background-image: url(' + c.thumb + ');'">
@ -349,7 +255,7 @@
</div> </div>
</div> </div>
<div v-if="profileLayout == 'moment'"> <div v-if="layout == 'moment'" class="mt-3">
<div :class="momentBackground()" style="width:100%;min-height:274px;"> <div :class="momentBackground()" style="width:100%;min-height:274px;">
</div> </div>
<div class="bg-white border-bottom"> <div class="bg-white border-bottom">
@ -514,28 +420,34 @@
size="sm" size="sm"
body-class="list-group-flush p-0"> body-class="list-group-flush p-0">
<div class="list-group" v-if="relationship"> <div class="list-group" v-if="relationship">
<div v-if="!owner && !relationship.following" class="list-group-item cursor-pointer text-center font-weight-bold lead rounded text-primary" @click="followProfile"> <div class="list-group-item cursor-pointer text-center rounded text-dark" @click="copyProfileLink">
Copy Link
</div>
<div v-if="user && !owner && !relationship.following" class="list-group-item cursor-pointer text-center rounded text-dark" @click="followProfile">
Follow Follow
</div> </div>
<div v-if="!owner && relationship.following" class="list-group-item cursor-pointer text-center font-weight-bold lead rounded" @click="followProfile"> <div v-if="user && !owner && relationship.following" class="list-group-item cursor-pointer text-center rounded" @click="followProfile">
Unfollow Unfollow
</div> </div>
<div v-if="!owner && !relationship.muting" class="list-group-item cursor-pointer text-center font-weight-bold lead rounded" @click="muteProfile"> <div v-if="user && !owner && !relationship.muting" class="list-group-item cursor-pointer text-center rounded" @click="muteProfile">
Mute Mute
</div> </div>
<div v-if="!owner && relationship.muting" class="list-group-item cursor-pointer text-center font-weight-bold lead rounded" @click="unmuteProfile"> <div v-if="user && !owner && relationship.muting" class="list-group-item cursor-pointer text-center rounded" @click="unmuteProfile">
Unmute Unmute
</div> </div>
<div v-if="!owner" class="list-group-item cursor-pointer text-center font-weight-bold lead rounded text-danger" @click="reportProfile"> <div v-if="user && !owner" class="list-group-item cursor-pointer text-center rounded text-dark" @click="reportProfile">
Report User Report User
</div> </div>
<div v-if="!owner && !relationship.blocking" class="list-group-item cursor-pointer text-center font-weight-bold lead rounded text-danger" @click="blockProfile"> <div v-if="user && !owner && !relationship.blocking" class="list-group-item cursor-pointer text-center rounded text-dark" @click="blockProfile">
Block Block
</div> </div>
<div v-if="!owner && relationship.blocking" class="list-group-item cursor-pointer text-center font-weight-bold lead rounded text-danger" @click="unblockProfile"> <div v-if="user && !owner && relationship.blocking" class="list-group-item cursor-pointer text-center rounded text-dark" @click="unblockProfile">
Unblock Unblock
</div> </div>
<div class="list-group-item cursor-pointer text-center font-weight-bold lead rounded text-muted" @click="$refs.visitorContextMenu.hide()"> <div v-if="user && owner" class="list-group-item cursor-pointer text-center rounded text-dark" @click="redirect('/settings/home')">
Settings
</div>
<div class="list-group-item cursor-pointer text-center rounded text-muted" @click="$refs.visitorContextMenu.hide()">
Close Close
</div> </div>
</div> </div>
@ -580,6 +492,20 @@
opacity: 0.6; opacity: 0.6;
text-shadow: 3px 3px 16px #272634; text-shadow: 3px 3px 16px #272634;
} }
.font-size-16px {
font-size: 16px;
}
.profile-website {
color: #003569;
text-decoration: none;
font-weight: 600;
}
.nav-topbar .nav-link {
color: #999;
}
.nav-topbar .nav-link .small {
font-weight: 600;
}
</style> </style>
<script type="text/javascript"> <script type="text/javascript">
import VueMasonry from 'vue-masonry-css' import VueMasonry from 'vue-masonry-css'
@ -596,15 +522,16 @@
return { return {
ids: [], ids: [],
profile: {}, profile: {},
user: {}, user: false,
timeline: [], timeline: [],
timelinePage: 2, timelinePage: 2,
min_id: 0, min_id: 0,
max_id: 0, max_id: 0,
loading: true, loading: true,
owner: false, owner: false,
layout: this.profileLayout,
mode: 'grid', mode: 'grid',
modes: ['grid', 'list', 'masonry', 'bookmarks'], modes: ['grid', 'collections', 'bookmarks'],
modalStatus: false, modalStatus: false,
relationship: {}, relationship: {},
followers: [], followers: [],
@ -618,29 +545,36 @@
bookmarks: [], bookmarks: [],
bookmarksPage: 2, bookmarksPage: 2,
collections: [], collections: [],
collectionsPage: 2 collectionsPage: 2,
isMobile: false
} }
}, },
beforeMount() { beforeMount() {
if(window.outerWidth < 576) {
$('nav.navbar').hide();
this.isMobile = true;
}
this.fetchRelationships(); this.fetchRelationships();
this.fetchProfile(); this.fetchProfile();
if(window.outerWidth < 576 && window.history.length > 2) {
$('nav.navbar').hide();
$('body').prepend('<div class="bg-white p-3 border-bottom"><div class="d-flex justify-content-between align-items-center"><div onclick="window.history.back();"><i class="fas fa-chevron-left fa-lg"></i></div><div class="font-weight-bold">' + this.profileUsername + '</div><div><i class="fas fa-chevron-right text-white fa-lg"></i></div></div></div>');
}
let u = new URLSearchParams(window.location.search); let u = new URLSearchParams(window.location.search);
if(u.has('ui') && u.get('ui') == 'moment' && this.profileLayout != 'moment') { if(u.has('ui') && u.get('ui') == 'moment' && this.layout != 'moment') {
this.profileLayout = 'moment'; this.layout = 'moment';
} }
if(u.has('ui') && u.get('ui') == 'metro' && this.profileLayout != 'metro') { if(u.has('ui') && u.get('ui') == 'metro' && this.layout != 'metro') {
this.profileLayout = 'metro'; this.layout = 'metro';
}
if(this.layout == 'metro' && u.has('t')) {
if(this.modes.indexOf(u.get('t')) != -1) {
if(u.get('t') == 'bookmarks') {
return;
}
this.mode = u.get('t');
}
} }
},
mounted() {
}, },
updated() { updated() {
$('[data-toggle="tooltip"]').tooltip();
}, },
methods: { methods: {
@ -672,11 +606,9 @@
this.loading = false; this.loading = false;
this.loadSponsor(); this.loadSponsor();
}).catch(err => { }).catch(err => {
swal( swal('Oops, something went wrong',
'Oops, something went wrong',
'Please release the page.', 'Please release the page.',
'error' 'error');
);
}); });
}, },
@ -1028,10 +960,12 @@
return o; return o;
}, },
followProfile() { followProfile($event) {
if($('body').hasClass('loggedIn') == false) { if($('body').hasClass('loggedIn') == false) {
return; return;
} }
$event.target.setAttribute('disabled','');
$event.target.blur();
axios.post('/i/follow', { axios.post('/i/follow', {
item: this.profileId item: this.profileId
}).then(res => { }).then(res => {
@ -1045,6 +979,7 @@
this.profile.followers_count++; this.profile.followers_count++;
} }
this.relationship.following = !this.relationship.following; this.relationship.following = !this.relationship.following;
$event.target.removeAttribute('disabled');
}).catch(err => { }).catch(err => {
if(err.response.data.message) { if(err.response.data.message) {
swal('Error', err.response.data.message, 'error'); swal('Error', err.response.data.message, 'error');
@ -1149,9 +1084,6 @@
}, },
visitorMenu() { visitorMenu() {
if($('body').hasClass('loggedIn') == false) {
return;
}
this.$refs.visitorContextMenu.show(); this.$refs.visitorContextMenu.show();
}, },
@ -1189,6 +1121,21 @@
showSponsorModal() { showSponsorModal() {
this.$refs.sponsorModal.show(); this.$refs.sponsorModal.show();
},
goBack() {
if(window.history.length > 2) {
window.history.back();
return;
} else {
window.location.href = '/';
return;
}
},
copyProfileLink() {
navigator.clipboard.writeText(window.location.href);
this.$refs.visitorContextMenu.hide();
} }
} }
} }

View file

@ -73,13 +73,13 @@
{{status.account.username}} {{status.account.username}}
</a> </a>
<div class="text-right" style="flex-grow:1;"> <div class="text-right" style="flex-grow:1;">
<button class="btn btn-link text-dark no-caret dropdown-toggle py-0" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" title="Post options"> <button class="btn btn-link text-dark py-0" type="button" @click="ctxMenu(status)">
<span class="fas fa-ellipsis-v fa-lg text-muted"></span> <span class="fas fa-ellipsis-h text-dark"></span>
</button> </button>
<div class="dropdown-menu dropdown-menu-right"> <!-- <div class="dropdown-menu dropdown-menu-right">
<a class="dropdown-item font-weight-bold" :href="status.url">Go to post</a> <a class="dropdown-item font-weight-bold" :href="status.url">Go to post</a>
<!-- <a class="dropdown-item font-weight-bold" href="#">Share</a> <!-- <a class="dropdown-item font-weight-bold" href="#">Share</a>
<a class="dropdown-item font-weight-bold" href="#">Embed</a> --> <a class="dropdown-item font-weight-bold" href="#">Embed</a> ->
<span v-if="statusOwner(status) == false"> <span v-if="statusOwner(status) == false">
<a class="dropdown-item font-weight-bold" :href="reportUrl(status)">Report</a> <a class="dropdown-item font-weight-bold" :href="reportUrl(status)">Report</a>
<a class="dropdown-item font-weight-bold" v-on:click="muteProfile(status)">Mute Profile</a> <a class="dropdown-item font-weight-bold" v-on:click="muteProfile(status)">Mute Profile</a>
@ -110,7 +110,7 @@
</a> </a>
</span> </span>
</div> </div> -->
</div> </div>
</div> </div>
@ -384,6 +384,57 @@
</div> </div>
</div> </div>
</b-modal> --> </b-modal> -->
<b-modal ref="ctxModal"
id="ctx-modal"
hide-header
hide-footer
centered
rounded
size="sm"
body-class="list-group-flush p-0 rounded">
<div class="list-group text-center">
<div v-if="ctxMenuStatus && ctxMenuStatus.account.id != profile.id" class="list-group-item rounded cursor-pointer font-weight-bold text-danger" @click="ctxMenuReportPost()">Report inappropriate</div>
<div v-if="ctxMenuRelationship && ctxMenuRelationship.following" class="list-group-item rounded cursor-pointer font-weight-bold text-danger" @click="ctxMenuUnfollow()">Unfollow</div>
<div v-if="ctxMenuRelationship && !ctxMenuRelationship.following" class="list-group-item rounded cursor-pointer font-weight-bold text-primary" @click="ctxMenuFollow()">Follow</div>
<div class="list-group-item rounded cursor-pointer" @click="ctxMenuGoToPost()">Go to post</div>
<!-- <div class="list-group-item rounded cursor-pointer" @click="ctxMenuEmbed()">Embed</div>
<div class="list-group-item rounded cursor-pointer" @click="ctxMenuShare()">Share</div> -->
<div class="list-group-item rounded cursor-pointer" @click="ctxMenuCopyLink()">Copy Link</div>
<div class="list-group-item rounded cursor-pointer text-lighter" @click="closeCtxMenu()">Cancel</div>
</div>
</b-modal>
<b-modal ref="ctxShareModal"
id="ctx-share-modal"
title="Share"
hide-footer
centered
rounded
size="sm"
body-class="list-group-flush p-0 rounded text-center">
<div class="list-group-item rounded cursor-pointer border-top-0">Email</div>
<div class="list-group-item rounded cursor-pointer">Facebook</div>
<div class="list-group-item rounded cursor-pointer">Mastodon</div>
<div class="list-group-item rounded cursor-pointer">Pinterest</div>
<div class="list-group-item rounded cursor-pointer">Pixelfed</div>
<div class="list-group-item rounded cursor-pointer">Twitter</div>
<div class="list-group-item rounded cursor-pointer">VK</div>
<div class="list-group-item rounded cursor-pointer text-lighter" @click="closeCtxShareMenu()">Cancel</div>
</b-modal>
<b-modal ref="ctxEmbedModal"
id="ctx-embed-modal"
hide-header
hide-footer
centered
rounded
size="md"
body-class="p-2 rounded">
<div>
<textarea class="form-control disabled" rows="1" style="border: 1px solid #efefef; font-size: 14px; line-height: 17px; min-height: 37px; margin: 0 0 7px; resize: none; white-space: nowrap;" v-model="ctxEmbedPayload"></textarea>
<hr>
<button :class="copiedEmbed ? 'btn btn-primary btn-block btn-sm py-1 font-weight-bold disabed': 'btn btn-primary btn-block btn-sm py-1 font-weight-bold'" @click="ctxCopyEmbed" :disabled="copiedEmbed">{{copiedEmbed ? 'Embed Code Copied!' : 'Copy Embed Code'}}</button>
<p class="mb-0 px-2 small text-muted">By using this embed, you agree to our <a href="#">API Terms of Use</a>.</p>
</div>
</b-modal>
<b-modal <b-modal
id="lightbox" id="lightbox"
ref="lightboxModal" ref="lightboxModal"
@ -438,7 +489,7 @@
data() { data() {
return { return {
ids: [], ids: [],
config: {}, config: window.App.config,
page: 2, page: 2,
feed: [], feed: [],
profile: {}, profile: {},
@ -470,23 +521,23 @@
showHashtagPosts: false, showHashtagPosts: false,
hashtagPosts: [], hashtagPosts: [],
hashtagPostsName: '', hashtagPostsName: '',
ctxMenuStatus: false,
ctxMenuRelationship: false,
ctxEmbedPayload: false,
copiedEmbed: false
} }
}, },
beforeMount() { beforeMount() {
axios.get('/api/v2/config') this.fetchProfile();
.then(res => { this.fetchTimelineApi();
this.config = res.data;
this.fetchProfile();
this.fetchTimelineApi();
// if(this.config.announcement.enabled == true) { // if(this.config.announcement.enabled == true) {
// let msg = $('<div>') // let msg = $('<div>')
// .addClass('alert alert-warning mb-0 rounded-0 text-center font-weight-bold') // .addClass('alert alert-warning mb-0 rounded-0 text-center font-weight-bold')
// .html(this.config.announcement.message); // .html(this.config.announcement.message);
// $('body').prepend(msg); // $('body').prepend(msg);
// } // }
});
}, },
mounted() { mounted() {
@ -604,7 +655,7 @@
if (res.data.length && this.loading == false) { if (res.data.length && this.loading == false) {
let data = res.data; let data = res.data;
let self = this; let self = this;
data.forEach(d => { data.forEach((d, index) => {
if(self.ids.indexOf(d.id) == -1) { if(self.ids.indexOf(d.id) == -1) {
self.feed.push(d); self.feed.push(d);
self.ids.push(d.id); self.ids.push(d.id);
@ -669,13 +720,15 @@
if($('body').hasClass('loggedIn') == false) { if($('body').hasClass('loggedIn') == false) {
return; return;
} }
let count = status.favourites_count;
status.favourited = !status.favourited;
axios.post('/i/like', { axios.post('/i/like', {
item: status.id item: status.id
}).then(res => { }).then(res => {
status.favourites_count = res.data.count; status.favourites_count = res.data.count;
status.favourited = !status.favourited;
}).catch(err => { }).catch(err => {
status.favourited = !status.favourited;
status.favourites_count = count;
swal('Error', 'Something went wrong, please try again later.', 'error'); swal('Error', 'Something went wrong, please try again later.', 'error');
}); });
}, },
@ -809,7 +862,6 @@
moderatePost(status, action, $event) { moderatePost(status, action, $event) {
let username = status.account.username; let username = status.account.username;
console.log('action: ' + action + ' status id' + status.id);
switch(action) { switch(action) {
case 'autocw': case 'autocw':
let msg = 'Are you sure you want to enforce CW for ' + username + ' ?'; let msg = 'Are you sure you want to enforce CW for ' + username + ' ?';
@ -1159,7 +1211,97 @@
}) })
}) })
},
ctxMenu(status) {
this.ctxMenuStatus = status;
let payload = '<div class="pixlfed-media" data-id="'+ this.ctxMenuStatus.id + '"></div><script ';
payload += 'src="https://pixelfed.dev/js/embed.js" async><';
payload += '/script>';
this.ctxEmbedPayload = payload;
if(status.account.id == this.profile.id) {
this.$refs.ctxModal.show();
} else {
axios.get('/api/v1/accounts/relationships', {
params: {
'id[]': status.account.id
}
}).then(res => {
this.ctxMenuRelationship = res.data[0];
this.$refs.ctxModal.show();
});
}
},
closeCtxMenu(truncate) {
this.copiedEmbed = false;
this.ctxMenuStatus = false;
this.ctxMenuRelationship = false;
this.$refs.ctxModal.hide();
},
ctxMenuCopyLink() {
let status = this.ctxMenuStatus;
navigator.clipboard.writeText(status.url);
this.closeCtxMenu();
return;
},
ctxMenuGoToPost() {
let status = this.ctxMenuStatus;
window.location.href = status.url;
this.closeCtxMenu();
return;
},
ctxMenuFollow() {
axios.post('/i/follow', {
item: this.ctxMenuStatus.account.id
}).then(res => {
let username = this.ctxMenuStatus.account.acct;
this.closeCtxMenu();
setTimeout(function() {
swal('Follow successful!', 'You are now following ' + username, 'success');
}, 500);
});
},
ctxMenuUnfollow() {
axios.post('/i/follow', {
item: this.ctxMenuStatus.account.id
}).then(res => {
let username = this.ctxMenuStatus.account.acct;
this.closeCtxMenu();
setTimeout(function() {
swal('Unfollow successful!', 'You are no longer following ' + username, 'success');
}, 500);
});
},
ctxMenuReportPost() {
window.location.href = '/i/report?type=post&id=' + this.ctxMenuStatus.id;
},
ctxMenuEmbed() {
this.$refs.ctxModal.hide();
this.$refs.ctxEmbedModal.show();
},
ctxMenuShare() {
this.$refs.ctxModal.hide();
this.$refs.ctxShareModal.show();
},
closeCtxShareMenu() {
this.$refs.ctxShareModal.hide();
this.$refs.ctxModal.show();
},
ctxCopyEmbed() {
navigator.clipboard.writeText(this.ctxEmbedPayload);
this.$refs.ctxEmbedModal.hide();
} }
} }
} }
</script> </script>

View file

@ -19,13 +19,18 @@
background: #ADAFAE !important; background: #ADAFAE !important;
} }
.btn-outline-light {
border-color: #E2E8F0 !important;
color: #E2E8F0 !important;
}
.bg-white, .bg-white,
.postPresenterContainer, .postPresenterContainer,
.postComponent .card-body.flex-grow-0.py-1, .postComponent .card-body.flex-grow-0.py-1,
.postComponent .reactions, .postComponent .reactions,
.postComponent .status-comments, .postComponent .status-comments,
.navbar-laravel { .navbar-laravel {
background: #282828 !important; background: #2D3748 !important;
} }
.postComponent .border-left { .postComponent .border-left {
@ -38,8 +43,8 @@
input, input,
textarea { textarea {
color: #ccc !important; color: #E2E8F0 !important;
background: #191919 !important; background: #4A5568 !important;
} }
.far, .fas, .far, .fas,
@ -51,8 +56,12 @@ textarea {
} }
.form-control.search-form-input { .form-control.search-form-input {
background: #060606 !important; color: #E2E8F0 !important;
color: #888 !important; background: #4A5568 !important;
}
.btn-outline-primary {
border-color: #4A5568 !important;
} }
@import "components/filters"; @import "components/filters";
@ -68,3 +77,7 @@ textarea {
@import '~vue-loading-overlay/dist/vue-loading.css'; @import '~vue-loading-overlay/dist/vue-loading.css';
@import "moment"; @import "moment";
.border {
border: 1px solid #4A5568 !important;
}

View file

@ -12,7 +12,7 @@ $gray-300: #dee2e6 !default;
$gray-400: #ADAFAE !default; $gray-400: #ADAFAE !default;
$gray-500: #888 !default; $gray-500: #888 !default;
$gray-600: #555 !default; $gray-600: #555 !default;
$gray-700: #282828 !default; $gray-700: #2D3748 !default;
$gray-800: #222 !default; $gray-800: #222 !default;
$gray-900: #212529 !default; $gray-900: #212529 !default;
$black: #000 !default; $black: #000 !default;
@ -42,7 +42,7 @@ $yiq-contrasted-threshold: 175 !default;
// Body // Body
$body-bg: #060606 !default; $body-bg: #1A202C !default;
$body-color: $gray-500 !default; $body-color: $gray-500 !default;
// Fonts // Fonts

View file

@ -3,42 +3,20 @@
@section('content') @section('content')
<div class="container"> <div class="container">
<div class="row"> <collection-component
<div class="col-12 mt-5 py-5"> collection-id="{{$collection->id}}"
<div class="text-center"> collection-title="{{$collection->title}}"
<h1>Collection</h1> collection-description="{{$collection->description}}"
<h4 class="text-muted">{{$collection->title}}</h4> collection-visibility="{{$collection->visibility}}"
@auth profile-id="{{$collection->profile_id}}"
@if($collection->profile_id == Auth::user()->profile_id) profile-username="{{$collection->profile->username}}"
<div class="text-right"> ></collection-component>
<form method="post" action="/api/local/collection/{{$collection->id}}">
@csrf
<input type="hidden" name="_method" value="DELETE">
<button type="submit" class="btn btn-outline-danger font-weight-bold btn-sm py-1">Delete</button>
</form>
</div>
@endif
@endauth
</div>
</div>
<div class="col-12">
<collection-component collection-id="{{$collection->id}}"></collection-component>
</div>
</div>
</div> </div>
@endsection @endsection
@push('styles')
<style type="text/css">
</style>
@endpush
@push('scripts') @push('scripts')
<script type="text/javascript" src="{{mix('js/compose.js')}}"></script> <script type="text/javascript" src="{{mix('js/compose.js')}}" async></script>
<script type="text/javascript" src="{{mix('js/collections.js')}}"></script> <script type="text/javascript" src="{{mix('js/collections.js')}}"></script>
<script type="text/javascript"> <script type="text/javascript">App.boot()</script>
new Vue({
el: '#content'
})
</script>
@endpush @endpush

View file

@ -1,17 +1,13 @@
@extends('layouts.app') @extends('layouts.app')
@section('content') @section('content')
<div class="container mt-5 pt-3"> <div class="container">
<section> <discover-component></discover-component>
<discover-component></discover-component>
</section>
</div> </div>
@endsection @endsection
@push('scripts') @push('scripts')
<script type="text/javascript" src="{{ mix('js/discover.js') }}"></script> <script type="text/javascript" src="{{ mix('js/discover.js') }}"></script>
<script type="text/javascript" src="{{ mix('js/compose.js') }}"></script> <script type="text/javascript" src="{{ mix('js/compose.js') }}"></script>
<script type="text/javascript"> <script type="text/javascript">App.boot();</script>
$(document).ready(function(){new Vue({el: '#content'});});
</script>
@endpush @endpush

View file

@ -27,6 +27,8 @@
<link rel="canonical" href="{{request()->url()}}"> <link rel="canonical" href="{{request()->url()}}">
<link href="{{ mix('css/app.css') }}" rel="stylesheet" data-stylesheet="light"> <link href="{{ mix('css/app.css') }}" rel="stylesheet" data-stylesheet="light">
@stack('styles') @stack('styles')
<script type="text/javascript">window.App = {}; window.App.config = {!!App\Util\Site\Config::json()!!}</script>
</head> </head>
<body class=""> <body class="">
@include('layouts.partial.noauthnav') @include('layouts.partial.noauthnav')

View file

@ -1,7 +1,6 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="{{ app()->getLocale() }}"> <html lang="{{ app()->getLocale() }}">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
@ -25,12 +24,17 @@
<link rel="apple-touch-icon" type="image/png" href="/img/favicon.png?v=2"> <link rel="apple-touch-icon" type="image/png" href="/img/favicon.png?v=2">
<link rel="canonical" href="{{request()->url()}}"> <link rel="canonical" href="{{request()->url()}}">
@if(request()->cookie('dark-mode')) @if(request()->cookie('dark-mode'))
<link href="{{ mix('css/appdark.css') }}" rel="stylesheet" data-stylesheet="dark"> <link href="{{ mix('css/appdark.css') }}" rel="stylesheet" data-stylesheet="dark">
@else @else
<link href="{{ mix('css/app.css') }}" rel="stylesheet" data-stylesheet="light"> <link href="{{ mix('css/app.css') }}" rel="stylesheet" data-stylesheet="light">
@endif @endif
@stack('styles') @stack('styles')
<script type="text/javascript">window.App = {config: {!!App\Util\Site\Config::json()!!}};</script>
</head> </head>
<body class="{{Auth::check()?'loggedIn':''}}"> <body class="{{Auth::check()?'loggedIn':''}}">
@include('layouts.partial.nav') @include('layouts.partial.nav')
@ -58,19 +62,19 @@
<div class="card card-body rounded-0 py-2 d-flex align-items-middle box-shadow" style="border-top:1px solid #F1F5F8"> <div class="card card-body rounded-0 py-2 d-flex align-items-middle box-shadow" style="border-top:1px solid #F1F5F8">
<ul class="nav nav-pills nav-fill"> <ul class="nav nav-pills nav-fill">
<li class="nav-item"> <li class="nav-item">
<a class="nav-link {{request()->is('/')?'text-dark':'text-muted'}}" href="/"><i class="fas fa-home fa-lg"></i></a> <a class="nav-link {{request()->is('/')?'text-dark':'text-lighter'}}" href="/"><i class="fas fa-home fa-lg"></i></a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link {{request()->is('timeline/public')?'text-dark':'text-muted'}}" href="/timeline/public"><i class="far fa-map fa-lg"></i></a> <a class="nav-link {{request()->is('discover')?'text-dark':'text-lighter'}}" href="/discover"><i class="fas fa-search fa-lg"></i></a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<div class="nav-link text-primary cursor-pointer" data-toggle="modal" data-target="#composeModal"><i class="fas fa-camera-retro fa-lg"></i></div> <div class="nav-link text-lighter cursor-pointer" data-toggle="modal" data-target="#composeModal"><i class="fas fa-camera fa-lg"></i></div>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link {{request()->is('discover')?'text-dark':'text-muted'}}" href="{{route('discover')}}"><i class="far fa-compass fa-lg"></i></a> <a class="nav-link {{request()->is('account/activity')?'text-dark':'text-lighter'}}" href="/account/activity"><i class="far fa-heart fa-lg"></i></a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link {{request()->is('account/activity')?'text-dark':'text-muted'}} tooltip-notification" href="/account/activity"><i class="far fa-bell fa-lg"></i></a> <a class="nav-link text-lighter" href="/i/me"><i class="far fa-user fa-lg"></i></a>
</li> </li>
</ul> </ul>
</div> </div>

View file

@ -25,6 +25,9 @@
<link rel="canonical" href="{{request()->url()}}"> <link rel="canonical" href="{{request()->url()}}">
<link href="{{ mix('css/app.css') }}" rel="stylesheet" data-stylesheet="light"> <link href="{{ mix('css/app.css') }}" rel="stylesheet" data-stylesheet="light">
@stack('styles') @stack('styles')
<script type="text/javascript">window.App = {}; window.App.config = {!!App\Util\Site\Config::json()!!}</script>
</head> </head>
<body class=""> <body class="">
<main id="content"> <main id="content">

View file

@ -7,7 +7,7 @@
<div class="collapse navbar-collapse" id="navbarSupportedContent"> <div class="collapse navbar-collapse" id="navbarSupportedContent">
@auth @auth
<ul class="navbar-nav mx-auto pr-3"> <ul class="navbar-nav d-none d-md-block mx-auto pr-3">
<form class="form-inline search-bar" method="get" action="/i/results"> <form class="form-inline search-bar" method="get" action="/i/results">
<div class="input-group"> <div class="input-group">
<input class="form-control" name="q" placeholder="{{__('navmenu.search')}}" aria-label="search" autocomplete="off" required> <input class="form-control" name="q" placeholder="{{__('navmenu.search')}}" aria-label="search" autocomplete="off" required>

View file

@ -22,14 +22,6 @@
<li class="mb-3 ">On the discover page, you will see a list of Category cards that links to each Discover Category.</li> <li class="mb-3 ">On the discover page, you will see a list of Category cards that links to each Discover Category.</li>
</ul> </ul>
</div> </div>
<div class="py-4">
<p class="font-weight-bold h5 pb-3">Personalized Discover <span class="badge badge-success">NEW</span></p>
<p>Discover posts based on hashtags you've used before. This feature might not be supported on every Pixelfed instance.</p>
<ul>
<li class="mb-3 ">Click the <i class="far fa-compass fa-sm"></i> icon.</li>
<li class="mb-3 ">Click on the card that says "For You" or <a href="/discover/personal">click here</a>.</li>
</ul>
</div>
<div class="card bg-primary border-primary" style="box-shadow: none !important;border: 3px solid #08d!important;"> <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">Discover Tips</div> <div class="card-header text-light font-weight-bold h4 p-4">Discover Tips</div>
<div class="card-body bg-white p-3"> <div class="card-body bg-white p-3">

View file

@ -23,6 +23,7 @@
<link rel="shortcut icon" type="image/png" href="/img/favicon.png?v=2"> <link rel="shortcut icon" type="image/png" href="/img/favicon.png?v=2">
<link rel="apple-touch-icon" type="image/png" href="/img/favicon.png?v=2"> <link rel="apple-touch-icon" type="image/png" href="/img/favicon.png?v=2">
<link href="{{ mix('css/landing.css') }}" rel="stylesheet"> <link href="{{ mix('css/landing.css') }}" rel="stylesheet">
<script type="text/javascript">window.App = {}; window.App.config = {!!App\Util\Site\Config::json()!!}</script>
</head> </head>
<body class=""> <body class="">
<main id="content"> <main id="content">

View file

@ -9,9 +9,5 @@
@push('scripts') @push('scripts')
<script type="text/javascript" src="{{ mix('js/timeline.js') }}"></script> <script type="text/javascript" src="{{ mix('js/timeline.js') }}"></script>
<script type="text/javascript" src="{{ mix('js/compose.js') }}"></script> <script type="text/javascript" src="{{ mix('js/compose.js') }}"></script>
<script type="text/javascript"> <script type="text/javascript">window.App.boot()</script>
new Vue({
el: '#content'
});
</script>
@endpush @endpush

View file

@ -108,7 +108,6 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact
Route::get('discover/tag', 'DiscoverController@getHashtags'); Route::get('discover/tag', 'DiscoverController@getHashtags');
}); });
Route::group(['prefix' => 'local'], function () { Route::group(['prefix' => 'local'], function () {
Route::get('i/follow-suggestions', 'ApiController@followSuggestions');
Route::post('status/compose', 'InternalApiController@compose')->middleware('throttle:maxPostsPerHour,60')->middleware('throttle:maxPostsPerDay,1440'); Route::post('status/compose', 'InternalApiController@compose')->middleware('throttle:maxPostsPerHour,60')->middleware('throttle:maxPostsPerDay,1440');
Route::get('exp/rec', 'ApiController@userRecommendations'); Route::get('exp/rec', 'ApiController@userRecommendations');
Route::post('discover/tag/subscribe', 'HashtagFollowController@store')->middleware('throttle:maxHashtagFollowsPerHour,60')->middleware('throttle:maxHashtagFollowsPerDay,1440');; Route::post('discover/tag/subscribe', 'HashtagFollowController@store')->middleware('throttle:maxHashtagFollowsPerHour,60')->middleware('throttle:maxHashtagFollowsPerDay,1440');;
@ -176,6 +175,8 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact
}); });
Route::get('collections/create', 'CollectionController@create'); Route::get('collections/create', 'CollectionController@create');
Route::get('me', 'ProfileController@meRedirect');
}); });
Route::group(['prefix' => 'account'], function () { Route::group(['prefix' => 'account'], function () {