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 859 additions and 798 deletions

View file

@ -118,7 +118,7 @@ class BaseApiController extends Controller
$since_id = $request->since_id ?? false;
$only_media = $request->only_media ?? false;
$user = Auth::user();
$account = Profile::findOrFail($id);
$account = Profile::whereNull('status')->findOrFail($id);
$statuses = $account->statuses()->getQuery();
if($only_media == true) {
$statuses = $statuses
@ -150,15 +150,6 @@ class BaseApiController extends Controller
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)
{
$this->validate($request, [
@ -197,14 +188,9 @@ class BaseApiController extends Controller
public function showTempMedia(Request $request, int $profileId, $mediaId)
{
if (!$request->hasValidSignature()) {
abort(401);
}
$profile = Auth::user()->profile;
if($profile->id !== $profileId) {
abort(403);
}
$media = Media::whereProfileId($profile->id)->findOrFail($mediaId);
abort_if(!$request->hasValidSignature(), 404);
abort_if(Auth::user()->profile_id !== $profileId, 404);
$media = Media::whereProfileId(Auth::user()->profile_id)->findOrFail($mediaId);
$path = storage_path('app/'.$media->media_path);
return response()->file($path);
}

View file

@ -10,6 +10,7 @@ use App\{
UserFilter
};
use Auth, Cache, Redis;
use App\Util\Site\Config;
use Illuminate\Http\Request;
use App\Services\SuggestionService;
@ -23,34 +24,7 @@ class ApiController extends BaseApiController
public function siteConfiguration(Request $request)
{
$res = 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')
],
];
});
return response()->json($res);
return response()->json(Config::get());
}
public function userRecommendations(Request $request)

View file

@ -34,7 +34,7 @@ class CollectionController extends Controller
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) {
abort(404);
}
@ -100,7 +100,11 @@ class CollectionController extends Controller
$collection->items()->delete();
$collection->delete();
return 200;
if($request->wantsJson()) {
return 200;
}
return redirect('/');
}
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'));
}
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',
'updated_at'
)->where('id', $dir, $id)
->with('profile', 'hashtags', 'mentions')
->whereIn('type', ['photo', 'photo:album', 'video', 'video:album'])
->whereLocal(true)
->whereNull('uri')
@ -300,6 +301,7 @@ class PublicApiController extends Controller
'created_at',
'updated_at'
)->whereIn('type', ['photo', 'photo:album', 'video', 'video:album'])
->with('profile', 'hashtags', 'mentions')
->whereLocal(true)
->whereNull('uri')
->whereNotIn('profile_id', $filtered)
@ -378,6 +380,7 @@ class PublicApiController extends Controller
'created_at',
'updated_at'
)->whereIn('type', ['photo', 'photo:album', 'video', 'video:album'])
->with('profile', 'hashtags', 'mentions')
->where('id', $dir, $id)
->whereIn('profile_id', $following)
->whereNotIn('profile_id', $filtered)
@ -405,6 +408,7 @@ class PublicApiController extends Controller
'created_at',
'updated_at'
)->whereIn('type', ['photo', 'photo:album', 'video', 'video:album'])
->with('profile', 'hashtags', 'mentions')
->whereIn('profile_id', $following)
->whereNotIn('profile_id', $filtered)
->whereNull('in_reply_to_id')

View file

@ -52,6 +52,7 @@ class SearchController extends Controller
'entity' => [
'id' => $item->id,
'following' => $item->followedBy(Auth::user()->profile),
'follow_request' => $item->hasFollowRequestById(Auth::user()->profile_id),
'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;
} else {
$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()

View file

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

View file

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

View file

@ -7,20 +7,20 @@ use League\Fractal;
class Announce extends Fractal\TransformerAbstract
{
public function transform(Status $status)
{
return [
'@context' => 'https://www.w3.org/ns/activitystreams',
'id' => $status->permalink(),
'type' => 'Announce',
'actor' => $status->profile->permalink(),
'to' => ['https://www.w3.org/ns/activitystreams#Public'],
'cc' => [
$status->profile->permalink(),
$status->profile->follower_url ?? $status->profile->permalink('/followers')
],
'published' => $status->created_at->format(DATE_ISO8601),
'object' => $status->parent()->url(),
];
}
public function transform(Status $status)
{
return [
'@context' => 'https://www.w3.org/ns/activitystreams',
'id' => $status->permalink(),
'type' => 'Announce',
'actor' => $status->profile->permalink(),
'to' => ['https://www.w3.org/ns/activitystreams#Public'],
'cc' => [
$status->profile->permalink(),
$status->profile->follower_url ?? $status->profile->permalink('/followers')
],
'published' => $status->created_at->format(DATE_ISO8601),
'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;
use Auth;
use App\Profile;
use App\{
FollowRequest,
Profile
};
use League\Fractal;
class RelationshipTransformer extends Fractal\TransformerAbstract
@ -12,6 +15,12 @@ class RelationshipTransformer extends Fractal\TransformerAbstract
{
$auth = Auth::check();
$user = $auth ? Auth::user()->profile : false;
$requested = false;
if($user) {
$requested = FollowRequest::whereFollowerId($user->id)
->whereFollowingId($profile->id)
->exists();
}
return [
'id' => (string) $profile->id,
'following' => $auth ? $user->follows($profile) : false,
@ -19,7 +28,7 @@ class RelationshipTransformer extends Fractal\TransformerAbstract
'blocking' => $auth ? $user->blockedIds()->contains($profile->id) : false,
'muting' => $auth ? $user->mutedIds()->contains($profile->id) : false,
'muting_notifications' => null,
'requested' => null,
'requested' => $requested,
'domain_blocking' => null,
'showing_reblogs' => null,
'endorsed' => false

View file

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

File diff suppressed because one or more lines are too long

2
public/js/app.js vendored
View file

@ -1 +1 @@
(window.webpackJsonp=window.webpackJsonp||[]).push([[4],{"+lRy":function(e,n){},0:function(e,n,t){t("JO1w"),t("+lRy"),t("xWuY"),t("YfGV"),e.exports=t("RvBz")},"8oxB":function(e,n){var t,r,o=e.exports={};function i(){throw new Error("setTimeout has not been defined")}function u(){throw new Error("clearTimeout has not been defined")}function c(e){if(t===setTimeout)return setTimeout(e,0);if((t===i||!t)&&setTimeout)return t=setTimeout,setTimeout(e,0);try{return t(e,0)}catch(n){try{return t.call(null,e,0)}catch(n){return t.call(this,e,0)}}}!function(){try{t="function"==typeof setTimeout?setTimeout:i}catch(e){t=i}try{r="function"==typeof clearTimeout?clearTimeout:u}catch(e){r=u}}();var f,s=[],a=!1,l=-1;function d(){a&&f&&(a=!1,f.length?s=f.concat(s):l=-1,s.length&&w())}function w(){if(!a){var e=c(d);a=!0;for(var n=s.length;n;){for(f=s,s=[];++l<n;)f&&f[l].run();l=-1,n=s.length}f=null,a=!1,function(e){if(r===clearTimeout)return clearTimeout(e);if((r===u||!r)&&clearTimeout)return r=clearTimeout,clearTimeout(e);try{r(e)}catch(n){try{return r.call(null,e)}catch(n){return r.call(this,e)}}}(e)}}function h(e,n){this.fun=e,this.array=n}function p(){}o.nextTick=function(e){var n=new Array(arguments.length-1);if(arguments.length>1)for(var t=1;t<arguments.length;t++)n[t-1]=arguments[t];s.push(new h(e,n)),1!==s.length||a||c(w)},h.prototype.run=function(){this.fun.apply(null,this.array)},o.title="browser",o.browser=!0,o.env={},o.argv=[],o.version="",o.versions={},o.on=p,o.addListener=p,o.once=p,o.off=p,o.removeListener=p,o.removeAllListeners=p,o.emit=p,o.prependListener=p,o.prependOnceListener=p,o.listeners=function(e){return[]},o.binding=function(e){throw new Error("process.binding is not supported")},o.cwd=function(){return"/"},o.chdir=function(e){throw new Error("process.chdir is not supported")},o.umask=function(){return 0}},HijD:function(e,n,t){window._=t("LvDl"),window.Popper=t("8L3F").default,window.pixelfed=window.pixelfed||{},window.$=window.jQuery=t("EVdn"),t("SYky"),window.axios=t("vDqi"),window.axios.defaults.headers.common["X-Requested-With"]="XMLHttpRequest",t("KGuw");var r=document.head.querySelector('meta[name="csrf-token"]');r?window.axios.defaults.headers.common["X-CSRF-TOKEN"]=r.content:console.error("CSRF token not found: https://laravel.com/docs/csrf#csrf-x-csrf-token")},JO1w:function(e,n,t){t("HijD")},RvBz:function(e,n){},YfGV:function(e,n){},YuTi:function(e,n){e.exports=function(e){return e.webpackPolyfill||(e.deprecate=function(){},e.paths=[],e.children||(e.children=[]),Object.defineProperty(e,"loaded",{enumerable:!0,get:function(){return e.l}}),Object.defineProperty(e,"id",{enumerable:!0,get:function(){return e.i}}),e.webpackPolyfill=1),e}},xWuY:function(e,n){},yLpj:function(e,n){var t;t=function(){return this}();try{t=t||new Function("return this")()}catch(e){"object"==typeof window&&(t=window)}e.exports=t}},[[0,0,1]]]);
(window.webpackJsonp=window.webpackJsonp||[]).push([[4],{"+lRy":function(e,n){},0:function(e,n,t){t("JO1w"),t("+lRy"),t("xWuY"),t("YfGV"),e.exports=t("RvBz")},"8oxB":function(e,n){var t,o,r=e.exports={};function i(){throw new Error("setTimeout has not been defined")}function u(){throw new Error("clearTimeout has not been defined")}function c(e){if(t===setTimeout)return setTimeout(e,0);if((t===i||!t)&&setTimeout)return t=setTimeout,setTimeout(e,0);try{return t(e,0)}catch(n){try{return t.call(null,e,0)}catch(n){return t.call(this,e,0)}}}!function(){try{t="function"==typeof setTimeout?setTimeout:i}catch(e){t=i}try{o="function"==typeof clearTimeout?clearTimeout:u}catch(e){o=u}}();var f,l=[],s=!1,a=-1;function w(){s&&f&&(s=!1,f.length?l=f.concat(l):a=-1,l.length&&d())}function d(){if(!s){var e=c(w);s=!0;for(var n=l.length;n;){for(f=l,l=[];++a<n;)f&&f[a].run();a=-1,n=l.length}f=null,s=!1,function(e){if(o===clearTimeout)return clearTimeout(e);if((o===u||!o)&&clearTimeout)return o=clearTimeout,clearTimeout(e);try{o(e)}catch(n){try{return o.call(null,e)}catch(n){return o.call(this,e)}}}(e)}}function p(e,n){this.fun=e,this.array=n}function h(){}r.nextTick=function(e){var n=new Array(arguments.length-1);if(arguments.length>1)for(var t=1;t<arguments.length;t++)n[t-1]=arguments[t];l.push(new p(e,n)),1!==l.length||s||c(d)},p.prototype.run=function(){this.fun.apply(null,this.array)},r.title="browser",r.browser=!0,r.env={},r.argv=[],r.version="",r.versions={},r.on=h,r.addListener=h,r.once=h,r.off=h,r.removeListener=h,r.removeAllListeners=h,r.emit=h,r.prependListener=h,r.prependOnceListener=h,r.listeners=function(e){return[]},r.binding=function(e){throw new Error("process.binding is not supported")},r.cwd=function(){return"/"},r.chdir=function(e){throw new Error("process.chdir is not supported")},r.umask=function(){return 0}},JO1w:function(e,n,t){window._=t("LvDl"),window.Popper=t("8L3F").default,window.pixelfed=window.pixelfed||{},window.$=window.jQuery=t("EVdn"),t("SYky"),window.axios=t("vDqi"),window.axios.defaults.headers.common["X-Requested-With"]="XMLHttpRequest",t("KGuw");var o=document.head.querySelector('meta[name="csrf-token"]');o?window.axios.defaults.headers.common["X-CSRF-TOKEN"]=o.content:console.error("CSRF token not found."),window.App=window.App||{},window.App.boot=function(){new Vue({el:"#content"})}},RvBz:function(e,n){},YfGV:function(e,n){},YuTi:function(e,n){e.exports=function(e){return e.webpackPolyfill||(e.deprecate=function(){},e.paths=[],e.children||(e.children=[]),Object.defineProperty(e,"loaded",{enumerable:!0,get:function(){return e.l}}),Object.defineProperty(e,"id",{enumerable:!0,get:function(){return e.i}}),e.webpackPolyfill=1),e}},xWuY:function(e,n){},yLpj:function(e,n){var t;t=function(){return this}();try{t=t||new Function("return this")()}catch(e){"object"==typeof window&&(t=window)}e.exports=t}},[[0,0,1]]]);

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -3,24 +3,24 @@
"/js/vendor.js": "/js/vendor.js?id=383c6f227a3b8d8d1c71",
"/js/ace.js": "/js/ace.js?id=4a28163d5fd63e64d6af",
"/js/activity.js": "/js/activity.js?id=a414018a6c03ddd2a492",
"/js/app.js": "/js/app.js?id=2f034c84c06dbb3e511d",
"/js/app.js": "/js/app.js?id=e44ab72ffa230a7a56c1",
"/css/app.css": "/css/app.css?id=73af4bf8ced6e3c406ee",
"/css/appdark.css": "/css/appdark.css?id=59ce4dd28451ffddf15a",
"/css/appdark.css": "/css/appdark.css?id=10d573d6faab9ee2eeca",
"/css/landing.css": "/css/landing.css?id=385cadd3ef179fae26cc",
"/css/quill.css": "/css/quill.css?id=81604d62610b0dbffad6",
"/js/collectioncompose.js": "/js/collectioncompose.js?id=a117b4e0b1d2fd859de5",
"/js/collections.js": "/js/collections.js?id=93bac411f11eb701648f",
"/js/components.js": "/js/components.js?id=7e4df37c02f12db5ef96",
"/js/compose.js": "/js/compose.js?id=a51bfcdbfaee53cf1eda",
"/js/collections.js": "/js/collections.js?id=5c9926c1f532e17026fc",
"/js/components.js": "/js/components.js?id=b981ec12e26469676c4e",
"/js/compose.js": "/js/compose.js?id=f27e95963a805f49dd03",
"/js/developers.js": "/js/developers.js?id=a395f12c52bb0eada6ab",
"/js/discover.js": "/js/discover.js?id=f8da29f2b16ae5be93fd",
"/js/discover.js": "/js/discover.js?id=e8165d745727c3c71c62",
"/js/hashtag.js": "/js/hashtag.js?id=b4ffe6499880acf0591c",
"/js/loops.js": "/js/loops.js?id=017e807837b173d82724",
"/js/mode-dot.js": "/js/mode-dot.js?id=8224e306cf53e3336620",
"/js/profile.js": "/js/profile.js?id=ea657aeb8d50b124f7e5",
"/js/profile.js": "/js/profile.js?id=575df314fa611b303ef7",
"/js/quill.js": "/js/quill.js?id=9edfe94c043a1bc68860",
"/js/search.js": "/js/search.js?id=b1bd588d07e682f8fce5",
"/js/status.js": "/js/status.js?id=709b96bbcc47b605497b",
"/js/theme-monokai.js": "/js/theme-monokai.js?id=344fb8527bb66574e4cd",
"/js/timeline.js": "/js/timeline.js?id=2bf7f82e900bd72a73f9"
"/js/timeline.js": "/js/timeline.js?id=e67c9fc93ae46926e038"
}

View file

@ -11,5 +11,11 @@ let token = document.head.querySelector('meta[name="csrf-token"]');
if (token) {
window.axios.defaults.headers.common['X-CSRF-TOKEN'] = token.content;
} 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 Loading from 'vue-loading-overlay';
import VueTimeago from 'vue-timeago';
//import {Howl, Howler} from 'howler';
Vue.use(BootstrapVue);
Vue.use(InfiniteLoading);
@ -36,126 +35,8 @@ try {
}
window.filesize = require('filesize');
// window.Plyr = require('plyr');
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() {
$(function () {
$('[data-toggle="tooltip"]').tooltip()

View file

@ -1,47 +1,121 @@
<template>
<div>
<div class="row">
<div class="col-4 p-0 p-sm-2 p-md-3 p-xs-1" v-for="(s, index) in posts">
<a class="card info-overlay card-md-border-0" :href="s.url">
<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 == 'video'" class="float-right mr-3 post-icon"><i class="fas fa-video fa-2x"></i></span>
<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="square-content" v-bind:style="previewBackground(s)">
</div>
<div class="info-overlay-text">
<h5 class="text-white m-auto font-weight-bold">
<span>
<span class="far fa-heart fa-lg p-2 d-flex-inline"></span>
<span class="d-flex-inline">{{s.favourites_count}}</span>
</span>
<span>
<span class="fas fa-retweet fa-lg p-2 d-flex-inline"></span>
<span class="d-flex-inline">{{s.reblogs_count}}</span>
</span>
</h5>
</div>
<div class="w-100 h-100">
<div v-if="!loaded" style="height: 80vh;" class="d-flex justify-content-center align-items-center">
<img src="/img/pixelfed-icon-grey.svg" class="">
</div>
<div class="row mt-3" v-if="loaded">
<div class="col-12 p-0 mb-3">
<picture class="d-flex align-items-center justify-content-center">
<div class="dims"></div>
<div style="z-index:500;position: absolute;" class="text-white">
<p class="display-4 text-center pt-3">{{title || 'Untitled Collection'}}</p>
<p class="lead text-center mb-3">{{description}}</p>
<p class="text-center">
{{posts.length}} photos · by <a :href="'/' + profileUsername" class="font-weight-bold text-white">{{profileUsername}}</a>
</p>
<p v-if="owner == true" class="pt-3 text-center">
<span>
<button class="btn btn-outline-light btn-sm" @click.prevent="addToCollection">Add Photo</button>
&nbsp; &nbsp;
<button class="btn btn-outline-light btn-sm" @click.prevent="editCollection">Edit</button>
&nbsp; &nbsp;
<button class="btn btn-outline-light btn-sm" @click.prevent="deleteCollection">Delete</button>
</span>
</p>
</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>
<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>
</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">
import VueMasonry from 'vue-masonry-css'
Vue.use(VueMasonry);
export default {
props: ['collection-id'],
props: [
'collection-id',
'collection-title',
'collection-description',
'collection-visibility',
'profile-id',
'profile-username'
],
data() {
return {
loaded: false,
posts: [],
currentUser: false,
owner: false,
title: this.collectionTitle,
description: this.collectionDescription,
visibility: this.collectionVisibility,
photoId: ''
}
},
beforeMount() {
this.fetchCurrentUser();
this.fetchItems();
},
@ -49,10 +123,19 @@ export default {
},
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() {
axios.get('/api/local/collection/items/' + this.collectionId)
.then(res => {
this.posts = res.data;
this.loaded = true;
});
},
@ -64,6 +147,70 @@ export default {
let preview = this.previewUrl(status);
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>

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>
</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)">
<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 v-if="ids.length > 0">
@ -173,19 +178,20 @@
{{composeText.length}} / {{config.uploader.max_caption_length}}
</div>
<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 dropdown-toggle dropdown-toggle-split" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="sr-only">Toggle Dropdown</span>
</button>
<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 == '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>
<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> -->
<button class="btn btn-primary btn-sm font-weight-bold px-3" v-on:click="compose()">Publish</button>
</div>
</div>
</div>
@ -218,11 +224,7 @@
export default {
data() {
return {
config: {
uploader: {
media_types: '',
}
},
config: window.App.config,
profile: {},
composeText: '',
composeTextLength: 0,
@ -241,7 +243,6 @@ export default {
},
beforeMount() {
this.fetchConfig();
this.fetchProfile();
},
@ -290,20 +291,9 @@ export default {
['Willow','filter-willow'],
['X-Pro II','filter-xpro-ii']
];
},
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() {
axios.get('/api/v1/accounts/verify_credentials').then(res => {
this.profile = res.data;
@ -311,7 +301,6 @@ export default {
this.visibility = 'private';
}
}).catch(err => {
console.log(err)
});
},
@ -464,6 +453,11 @@ export default {
let data = res.data;
window.location.href = data;
}).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');
});
return;
@ -511,6 +505,18 @@ export default {
createCollection() {
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>
<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">
<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 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);">
<p class="text-white lead font-weight-bold mb-0">For You</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>
<section class="mb-5 section-explore">
<div class="profile-timeline">
<div class="loader text-center">
<div class="lds-ring"><div></div><div></div><div></div><div></div></div>
</div>
<div class="row d-none">
<div class="col-4 p-0 p-sm-2 p-md-3" v-for="post in posts">
<a class="card info-overlay card-md-border-0" :href="post.url">
<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>
<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>
</section>
<section class="mb-5 section-explore">
<div class="profile-timeline">
<div class="row p-0">
<div class="col-4 p-1 p-sm-2 p-md-3" v-for="post in posts">
<a class="card info-overlay card-md-border-0" :href="post.url">
<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>
<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>
</div>
</template>
<style type="text/css" scoped>
.discover-bar::-webkit-scrollbar {
display: none;
}
.card-disc {
flex: 0 0 160px;
width:160px;
height:100px;
background-size: cover !important;
}
.post-icon {
color: #fff;
position:relative;
margin-top: 10px;
z-index: 9;
opacity: 0.6;
text-shadow: 3px 3px 16px #272634;
}
.discover-bar::-webkit-scrollbar {
display: none;
}
.card-disc {
flex: 0 0 160px;
width:160px;
height:100px;
background-size: cover !important;
}
.post-icon {
color: #fff;
position:relative;
margin-top: 10px;
z-index: 9;
opacity: 0.6;
text-shadow: 3px 3px 16px #272634;
}
</style>
<script type="text/javascript">
export default {
data() {
return {
config: {},
posts: {},
trending: {},
categories: {},
allCategories: {},
}
},
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');
}
});
export default {
data() {
return {
loaded: false,
config: window.App.config,
posts: {},
trending: {},
categories: {},
allCategories: {},
searchTerm: '',
}
},
mounted() {
this.fetchData();
this.fetchCategories();
},
fetchCategories() {
axios.get('/api/v2/discover/categories')
.then(res => {
this.allCategories = res.data;
this.categories = res.data;
});
},
methods: {
fetchData() {
axios.get('/api/v2/discover/posts')
.then((res) => {
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>
<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 class="container">
<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="">
</div>
<div v-if="!loading && !warning">
<div v-if="profileLayout == 'metro'">
<div class="bg-white py-5 border-bottom">
<div class="container">
<div v-if="layout == 'metro'" class="container">
<div :class="isMobile ? 'pt-5' : 'pt-5 border-bottom'">
<div class="container px-0">
<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="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>
<span class="float-right mb-0" v-if="!loading && profile.id != user.id && user.hasOwnProperty('id')">
<a class="fas fa-cog fa-lg text-muted text-decoration-none" href="#" @click.prevent="visitorMenu"></a>
</span>
</p>
<p v-if="!loading && profile.id == user.id && user.hasOwnProperty('id')">
<a class="btn btn-outline-dark py-0 px-4 mt-3" href="/settings/home">Edit Profile</a>
</p>
<div v-if="profile.id != user.id && user.hasOwnProperty('id')">
<p class="mt-3 mb-0" v-if="relationship.following == true">
<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>
</p>
<p class="mt-3 mb-0" v-if="!relationship.following">
<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>
<!-- MOBILE PROFILE PICTURE -->
<div class="d-block d-md-none mt-n3 mb-3">
<div class="row">
<div class="col-4">
<img :alt="profileUsername + '\'s profile picture'" class="rounded-circle border mr-2" :src="profile.avatar" width="77px" height="77px">
</div>
<div class="col-8">
<div class="d-block d-md-none mt-3 py-2">
<ul class="nav d-flex justify-content-between">
<li class="nav-item">
<div class="font-weight-light">
<span class="text-dark text-center">
<p class="font-weight-bold mb-0">{{profile.statuses_count}}</p>
<p class="text-muted mb-0 small">Posts</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 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">
<button type="button" @click="showSponsorModal" class="btn btn-outline-secondary font-weight-bold py-0">
<i class="fas fa-heart text-danger"></i>
@ -61,110 +84,87 @@
</div>
<div class="col-12 col-md-8 d-flex align-items-center">
<div class="profile-details">
<div class="d-none d-md-flex username-bar pb-2 align-items-center">
<span class="font-weight-ultralight h3">{{profile.username}}</span>
<span class="pl-4" v-if="profile.is_admin">
<span class="btn btn-outline-secondary font-weight-bold py-0">ADMIN</span>
</span>
<span class="pl-4">
<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>
<div class="d-none d-md-flex username-bar pb-3 align-items-center">
<span class="font-weight-ultralight h3 mb-0">{{profile.username}}</span>
<span class="pl-1 pb-2" v-if="profile.is_admin" title="Admin Account" data-toggle="tooltip">
<i class="fas fa-certificate fa-lg text-primary">
</i>
<i class="fas fa-check text-white fa-sm" style="font-size:9px;margin-left: -1.1rem;padding-bottom: 0.6rem;"></i>
</span>
<span v-if="profile.id != user.id && user.hasOwnProperty('id')">
<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 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 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 class="d-none d-md-inline-flex profile-stats pb-3 lead">
<div class="font-weight-light pr-5">
<a class="text-dark" :href="profile.url">
<span class="font-weight-bold">{{profile.statuses_count}}</span>
Posts
</a>
</div>
<div v-if="profileSettings.followers.count" class="font-weight-light pr-5">
<a class="text-dark cursor-pointer" v-on:click="followersModal()">
<span class="font-weight-bold">{{profile.followers_count}}</span>
Followers
</a>
</div>
<div v-if="profileSettings.following.count" class="font-weight-light">
<a class="text-dark cursor-pointer" v-on:click="followingModal()">
<span class="font-weight-bold">{{profile.following_count}}</span>
Following
</a>
<div class="font-size-16px">
<div class="d-none d-md-inline-flex profile-stats pb-3">
<div class="font-weight-light pr-5">
<span class="text-dark">
<span class="font-weight-bold">{{profile.statuses_count}}</span>
Posts
</span>
</div>
<div v-if="profileSettings.followers.count" class="font-weight-light pr-5">
<a class="text-dark cursor-pointer" v-on:click="followersModal()">
<span class="font-weight-bold">{{profile.followers_count}}</span>
Followers
</a>
</div>
<div v-if="profileSettings.following.count" class="font-weight-light">
<a class="text-dark cursor-pointer" v-on:click="followingModal()">
<span class="font-weight-bold">{{profile.following_count}}</span>
Following
</a>
</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>
<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 class="d-block d-md-none bg-white my-0 py-2 border-bottom">
<ul class="nav d-flex justify-content-center">
<li class="nav-item">
<div class="font-weight-light">
<span class="text-dark text-center">
<p class="font-weight-bold mb-0">{{profile.statuses_count}}</p>
<p class="text-muted mb-0">Posts</p>
</span>
</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 class="d-block d-md-none my-0 pt-3 border-bottom">
<p v-if="user && user.hasOwnProperty('id')" class="pt-3">
<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>
<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>
<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>
<!-- <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>
<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> -->
</p>
</div>
<div class="bg-white">
<div class="">
<ul class="nav nav-topbar d-flex justify-content-center border-0">
<li class="nav-item">
<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 class="nav-item border-top">
<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>
</li>
<li class="nav-item px-3">
<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 class="nav-item px-0 border-top">
<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>
</li>
<li class="nav-item pr-3">
<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>
</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 v-if="owner" class="nav-item border-top">
<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>
</ul>
</div>
<div class="container">
<div class="container px-0">
<div class="profile-timeline mt-md-4">
<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">
<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>
@ -188,115 +188,21 @@
</a>
</div>
</div>
<div class="row" v-if="mode == 'list'">
<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 v-if="mode == 'grid' != -1 && timeline.length == 0">
<div class="py-5 text-center text-muted">
<p><i class="fas fa-camera-retro fa-2x"></i></p>
<p class="h2 font-weight-light pt-3">No posts yet</p>
</div>
</div>
<div v-if="timeline.length && ['grid','list'].indexOf(mode) != -1">
<div v-if="timeline.length && mode == 'grid'">
<infinite-loading @infinite="infiniteTimeline">
<div slot="no-more"></div>
<div slot="no-results"></div>
</infinite-loading>
</div>
<div class="row" v-if="mode == 'bookmarks'">
<div v-if="bookmarks.length">
<div class="col-4 p-0 p-sm-2 p-md-3 p-xs-1" v-for="(s, index) in bookmarks">
<div v-if="mode == 'bookmarks'">
<div v-if="bookmarks.length" class="row">
<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">
<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>
@ -323,13 +229,13 @@
<div v-else class="col-12">
<div class="py-5 text-center text-muted">
<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 class="col-12" v-if="mode == 'collections'">
<div v-if="mode == 'collections'">
<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">
<div class="square">
<div class="square-content" v-bind:style="'background-image: url(' + c.thumb + ');'">
@ -349,7 +255,7 @@
</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>
<div class="bg-white border-bottom">
@ -514,28 +420,34 @@
size="sm"
body-class="list-group-flush p-0">
<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
</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
</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
</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
</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
</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
</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
</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
</div>
</div>
@ -580,6 +492,20 @@
opacity: 0.6;
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>
<script type="text/javascript">
import VueMasonry from 'vue-masonry-css'
@ -596,15 +522,16 @@
return {
ids: [],
profile: {},
user: {},
user: false,
timeline: [],
timelinePage: 2,
min_id: 0,
max_id: 0,
loading: true,
owner: false,
layout: this.profileLayout,
mode: 'grid',
modes: ['grid', 'list', 'masonry', 'bookmarks'],
modes: ['grid', 'collections', 'bookmarks'],
modalStatus: false,
relationship: {},
followers: [],
@ -618,29 +545,36 @@
bookmarks: [],
bookmarksPage: 2,
collections: [],
collectionsPage: 2
collectionsPage: 2,
isMobile: false
}
},
beforeMount() {
if(window.outerWidth < 576) {
$('nav.navbar').hide();
this.isMobile = true;
}
this.fetchRelationships();
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);
if(u.has('ui') && u.get('ui') == 'moment' && this.profileLayout != 'moment') {
this.profileLayout = 'moment';
if(u.has('ui') && u.get('ui') == 'moment' && this.layout != 'moment') {
this.layout = 'moment';
}
if(u.has('ui') && u.get('ui') == 'metro' && this.profileLayout != 'metro') {
this.profileLayout = 'metro';
if(u.has('ui') && u.get('ui') == 'metro' && this.layout != '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() {
$('[data-toggle="tooltip"]').tooltip();
},
methods: {
@ -672,11 +606,9 @@
this.loading = false;
this.loadSponsor();
}).catch(err => {
swal(
'Oops, something went wrong',
swal('Oops, something went wrong',
'Please release the page.',
'error'
);
'error');
});
},
@ -1028,10 +960,12 @@
return o;
},
followProfile() {
followProfile($event) {
if($('body').hasClass('loggedIn') == false) {
return;
}
$event.target.setAttribute('disabled','');
$event.target.blur();
axios.post('/i/follow', {
item: this.profileId
}).then(res => {
@ -1045,6 +979,7 @@
this.profile.followers_count++;
}
this.relationship.following = !this.relationship.following;
$event.target.removeAttribute('disabled');
}).catch(err => {
if(err.response.data.message) {
swal('Error', err.response.data.message, 'error');
@ -1149,9 +1084,6 @@
},
visitorMenu() {
if($('body').hasClass('loggedIn') == false) {
return;
}
this.$refs.visitorContextMenu.show();
},
@ -1189,6 +1121,21 @@
showSponsorModal() {
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}}
</a>
<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">
<span class="fas fa-ellipsis-v fa-lg text-muted"></span>
<button class="btn btn-link text-dark py-0" type="button" @click="ctxMenu(status)">
<span class="fas fa-ellipsis-h text-dark"></span>
</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="#">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">
<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>
@ -110,7 +110,7 @@
</a>
</span>
</div>
</div> -->
</div>
</div>
@ -384,6 +384,57 @@
</div>
</div>
</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
id="lightbox"
ref="lightboxModal"
@ -438,7 +489,7 @@
data() {
return {
ids: [],
config: {},
config: window.App.config,
page: 2,
feed: [],
profile: {},
@ -470,23 +521,23 @@
showHashtagPosts: false,
hashtagPosts: [],
hashtagPostsName: '',
ctxMenuStatus: false,
ctxMenuRelationship: false,
ctxEmbedPayload: false,
copiedEmbed: false
}
},
beforeMount() {
axios.get('/api/v2/config')
.then(res => {
this.config = res.data;
this.fetchProfile();
this.fetchTimelineApi();
this.fetchProfile();
this.fetchTimelineApi();
// if(this.config.announcement.enabled == true) {
// let msg = $('<div>')
// .addClass('alert alert-warning mb-0 rounded-0 text-center font-weight-bold')
// .html(this.config.announcement.message);
// $('body').prepend(msg);
// }
});
// if(this.config.announcement.enabled == true) {
// let msg = $('<div>')
// .addClass('alert alert-warning mb-0 rounded-0 text-center font-weight-bold')
// .html(this.config.announcement.message);
// $('body').prepend(msg);
// }
},
mounted() {
@ -604,7 +655,7 @@
if (res.data.length && this.loading == false) {
let data = res.data;
let self = this;
data.forEach(d => {
data.forEach((d, index) => {
if(self.ids.indexOf(d.id) == -1) {
self.feed.push(d);
self.ids.push(d.id);
@ -669,13 +720,15 @@
if($('body').hasClass('loggedIn') == false) {
return;
}
let count = status.favourites_count;
status.favourited = !status.favourited;
axios.post('/i/like', {
item: status.id
}).then(res => {
status.favourites_count = res.data.count;
status.favourited = !status.favourited;
}).catch(err => {
status.favourited = !status.favourited;
status.favourites_count = count;
swal('Error', 'Something went wrong, please try again later.', 'error');
});
},
@ -809,7 +862,6 @@
moderatePost(status, action, $event) {
let username = status.account.username;
console.log('action: ' + action + ' status id' + status.id);
switch(action) {
case 'autocw':
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>

View file

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

View file

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

View file

@ -3,42 +3,20 @@
@section('content')
<div class="container">
<div class="row">
<div class="col-12 mt-5 py-5">
<div class="text-center">
<h1>Collection</h1>
<h4 class="text-muted">{{$collection->title}}</h4>
@auth
@if($collection->profile_id == Auth::user()->profile_id)
<div class="text-right">
<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>
<collection-component
collection-id="{{$collection->id}}"
collection-title="{{$collection->title}}"
collection-description="{{$collection->description}}"
collection-visibility="{{$collection->visibility}}"
profile-id="{{$collection->profile_id}}"
profile-username="{{$collection->profile->username}}"
></collection-component>
</div>
@endsection
@push('styles')
<style type="text/css">
</style>
@endpush
@push('scripts')
<script type="text/javascript" src="{{mix('js/compose.js')}}"></script>
<script type="text/javascript" src="{{mix('js/collections.js')}}"></script>
<script type="text/javascript">
new Vue({
el: '#content'
})
</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">App.boot()</script>
@endpush

View file

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

View file

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

View file

@ -1,7 +1,6 @@
<!DOCTYPE html>
<html lang="{{ app()->getLocale() }}">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<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="canonical" href="{{request()->url()}}">
@if(request()->cookie('dark-mode'))
<link href="{{ mix('css/appdark.css') }}" rel="stylesheet" data-stylesheet="dark">
@else
<link href="{{ mix('css/app.css') }}" rel="stylesheet" data-stylesheet="light">
@endif
@stack('styles')
<script type="text/javascript">window.App = {config: {!!App\Util\Site\Config::json()!!}};</script>
</head>
<body class="{{Auth::check()?'loggedIn':''}}">
@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">
<ul class="nav nav-pills nav-fill">
<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 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 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 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 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>
</ul>
</div>

View file

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

View file

@ -7,7 +7,7 @@
<div class="collapse navbar-collapse" id="navbarSupportedContent">
@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">
<div class="input-group">
<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>
</ul>
</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-header text-light font-weight-bold h4 p-4">Discover Tips</div>
<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="apple-touch-icon" type="image/png" href="/img/favicon.png?v=2">
<link href="{{ mix('css/landing.css') }}" rel="stylesheet">
<script type="text/javascript">window.App = {}; window.App.config = {!!App\Util\Site\Config::json()!!}</script>
</head>
<body class="">
<main id="content">

View file

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

View file

@ -108,7 +108,6 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact
Route::get('discover/tag', 'DiscoverController@getHashtags');
});
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::get('exp/rec', 'ApiController@userRecommendations');
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('me', 'ProfileController@meRedirect');
});
Route::group(['prefix' => 'account'], function () {