Merge pull request #2496 from pixelfed/staging

Staging
This commit is contained in:
daniel 2020-12-13 23:00:20 -07:00 committed by GitHub
commit d652de6f1b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 427 additions and 159 deletions

View file

@ -135,6 +135,10 @@
- Updated NotificationTransformer, add missing types. ([3a428366](https://github.com/pixelfed/pixelfed/commit/3a428366)) - Updated NotificationTransformer, add missing types. ([3a428366](https://github.com/pixelfed/pixelfed/commit/3a428366))
- Updated StatusService, fix json bug. ([1ea2db74](https://github.com/pixelfed/pixelfed/commit/1ea2db74)) - Updated StatusService, fix json bug. ([1ea2db74](https://github.com/pixelfed/pixelfed/commit/1ea2db74))
- Updated NotificationTransformer, handle tagged deletes. ([881fa865](https://github.com/pixelfed/pixelfed/commit/881fa865)) - Updated NotificationTransformer, handle tagged deletes. ([881fa865](https://github.com/pixelfed/pixelfed/commit/881fa865))
- Updated horizon config, add new default values. ([90c8a721](https://github.com/pixelfed/pixelfed/commit/90c8a721))
- Updated ComposeModal, add maxlength attribute to alt text input. Fixes ([#2490](https://github.com/pixelfed/pixelfed/issues/2490)). ([526b5531](https://github.com/pixelfed/pixelfed/commit/526b5531))
- Updated PublicApiController, add state endpoint. ([9fc5a80c](https://github.com/pixelfed/pixelfed/commit/9fc5a80c))
- Updated PostComponent, add reply modal. ([a10d851f](https://github.com/pixelfed/pixelfed/commit/a10d851f))
## [v0.10.9 (2020-04-17)](https://github.com/pixelfed/pixelfed/compare/v0.10.8...v0.10.9) ## [v0.10.9 (2020-04-17)](https://github.com/pixelfed/pixelfed/compare/v0.10.8...v0.10.9)
### Added ### Added

View file

@ -92,32 +92,47 @@ class PublicApiController extends Controller
$item = new Fractal\Resource\Item($status, new StatusStatelessTransformer()); $item = new Fractal\Resource\Item($status, new StatusStatelessTransformer());
$res = [ $res = [
'status' => $this->fractal->createData($item)->toArray(), 'status' => $this->fractal->createData($item)->toArray(),
'user' => [],
'likes' => [],
'shares' => [],
'reactions' => [
'liked' => false,
'shared' => false,
'bookmarked' => false,
],
]; ];
return response()->json($res, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES); return $res;
}); });
return $res; return response()->json($res);
} }
$item = new Fractal\Resource\Item($status, new StatusTransformer()); $item = new Fractal\Resource\Item($status, new StatusStatelessTransformer());
$res = [ $res = [
'status' => $this->fractal->createData($item)->toArray(), 'status' => $this->fractal->createData($item)->toArray(),
'user' => $this->getUserData($request->user()), ];
'likes' => $this->getLikes($status), return response()->json($res);
'shares' => $this->getShares($status), }
public function statusState(Request $request, $username, int $postid)
{
$profile = Profile::whereUsername($username)->whereNull('status')->firstOrFail();
$status = Status::whereProfileId($profile->id)->findOrFail($postid);
$this->scopeCheck($profile, $status);
if(!Auth::check()) {
$res = [
'user' => [],
'likes' => [],
'shares' => [],
'reactions' => [
'liked' => false,
'shared' => false,
'bookmarked' => false,
],
];
return response()->json($res);
}
$res = [
'user' => $this->getUserData($request->user()),
'likes' => [],
'shares' => [],
'reactions' => [ 'reactions' => [
'liked' => $status->liked(), 'liked' => (bool) $status->liked(),
'shared' => $status->shared(), 'shared' => (bool) $status->shared(),
'bookmarked' => $status->bookmarked(), 'bookmarked' => (bool) $status->bookmarked(),
], ],
]; ];
return response()->json($res, 200, [], JSON_PRETTY_PRINT); return response()->json($res);
} }
public function statusComments(Request $request, $username, int $postId) public function statusComments(Request $request, $username, int $postId)

View file

@ -97,7 +97,29 @@ return [
'trim' => [ 'trim' => [
'recent' => 60, 'recent' => 60,
'pending' => 60,
'completed' => 60,
'recent_failed' => 10080,
'failed' => 10080, 'failed' => 10080,
'monitored' => 10080,
],
/*
|--------------------------------------------------------------------------
| Metrics
|--------------------------------------------------------------------------
|
| Here you can configure how many snapshots should be kept to display in
| the metrics graph. This will get used in combination with Horizon's
| `horizon:snapshot` schedule to define how long to retain metrics.
|
*/
'metrics' => [
'trim_snapshots' => [
'job' => 24,
'queue' => 24,
],
], ],
/* /*
@ -142,21 +164,25 @@ return [
'environments' => [ 'environments' => [
'production' => [ 'production' => [
'supervisor-1' => [ 'supervisor-1' => [
'connection' => 'redis', 'connection' => 'redis',
'queue' => ['high', 'default', 'feed'], 'queue' => ['high', 'default', 'feed'],
'balance' => 'auto', 'balance' => 'auto',
'processes' => 20, 'maxProcesses' => 20,
'tries' => 3, 'memory' => 128,
'tries' => 3,
'nice' => 0,
], ],
], ],
'local' => [ 'local' => [
'supervisor-1' => [ 'supervisor-1' => [
'connection' => 'redis', 'connection' => 'redis',
'queue' => ['high', 'default', 'feed'], 'queue' => ['high', 'default', 'feed'],
'balance' => 'auto', 'balance' => 'auto',
'processes' => 20, 'maxProcesses' => 20,
'tries' => 3, 'memory' => 128,
'tries' => 3,
'nice' => 0,
], ],
], ],
], ],

BIN
public/js/compose.js vendored

Binary file not shown.

Binary file not shown.

BIN
public/js/profile.js vendored

Binary file not shown.

BIN
public/js/rempos.js vendored

Binary file not shown.

BIN
public/js/status.js vendored

Binary file not shown.

BIN
public/js/timeline.js vendored

Binary file not shown.

Binary file not shown.

View file

@ -327,6 +327,7 @@
</div> </div>
<p class="font-weight-bold text-center small text-muted pt-3 mb-0">When you tag someone, they are sent a notification.<br>For more information on tagging, <a href="#" class="text-primary" @click.prevent="showTagHelpCard()">click here</a>.</p> <p class="font-weight-bold text-center small text-muted pt-3 mb-0">When you tag someone, they are sent a notification.<br>For more information on tagging, <a href="#" class="text-primary" @click.prevent="showTagHelpCard()">click here</a>.</p>
</div> </div>
<div v-if="page == 'tagPeopleHelp'" class="w-100 h-100 p-3"> <div v-if="page == 'tagPeopleHelp'" class="w-100 h-100 p-3">
<p class="mb-0 text-center py-3 px-2 lead">Tagging someone is like mentioning them, with the option to make it private between you.</p> <p class="mb-0 text-center py-3 px-2 lead">Tagging someone is like mentioning them, with the option to make it private between you.</p>
<p class="mb-3 py-3 px-2 font-weight-lighter"> <p class="mb-3 py-3 px-2 font-weight-lighter">
@ -420,7 +421,7 @@
<div class="media"> <div class="media">
<img :src="m.preview_url" class="mr-3" width="50px" height="50px"> <img :src="m.preview_url" class="mr-3" width="50px" height="50px">
<div class="media-body"> <div class="media-body">
<textarea class="form-control" v-model="m.alt" placeholder="Add a media description here..."></textarea> <textarea class="form-control" v-model="m.alt" placeholder="Add a media description here..." maxlength="140"></textarea>
<p class="help-text small text-right text-muted mb-0">{{m.alt ? m.alt.length : 0}}/140</p> <p class="help-text small text-right text-muted mb-0">{{m.alt ? m.alt.length : 0}}/140</p>
</div> </div>
</div> </div>
@ -468,7 +469,7 @@
<div class="media-body"> <div class="media-body">
<div class="form-group"> <div class="form-group">
<label class="font-weight-bold text-muted small">Media Description</label> <label class="font-weight-bold text-muted small">Media Description</label>
<textarea class="form-control" v-model="media[carouselCursor].alt" placeholder="Add a media description here..."></textarea> <textarea class="form-control" v-model="media[carouselCursor].alt" placeholder="Add a media description here..." maxlength="140"></textarea>
<p class="help-text small text-muted mb-0 d-flex justify-content-between"> <p class="help-text small text-muted mb-0 d-flex justify-content-between">
<span>Describe your photo for people with visual impairments.</span> <span>Describe your photo for people with visual impairments.</span>
<span>{{media[carouselCursor].alt ? media[carouselCursor].alt.length : 0}}/140</span> <span>{{media[carouselCursor].alt ? media[carouselCursor].alt.length : 0}}/140</span>

View file

@ -35,24 +35,10 @@
</div> </div>
<div v-if="user != false" class="float-right"> <div v-if="user != false" class="float-right">
<div class="post-actions"> <div class="post-actions">
<div class="dropdown"> <div>
<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"> <button class="btn btn-link text-dark no-caret" title="Post options" @click="ctxMenu()">
<span class="fas fa-ellipsis-v text-muted"></span> <span class="fas fa-ellipsis-v text-muted"></span>
</button> </button>
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownMenuButton">
<a class="dropdown-item font-weight-bold" @click="copyPostUrl()">Copy Post Url</a>
<a class="dropdown-item font-weight-bold" @click="showEmbedPostModal()">Embed</a>
<div v-if="!owner()">
<a class="dropdown-item font-weight-bold" :href="reportUrl()">Report</a>
<a class="dropdown-item font-weight-bold" v-on:click="muteProfile()">Mute Profile</a>
<a class="dropdown-item font-weight-bold" v-on:click="blockProfile()">Block Profile</a>
</div>
<div v-if="ownerOrAdmin()">
<a class="dropdown-item font-weight-bold" href="#" v-on:click.prevent="toggleCommentVisibility">{{ showComments ? 'Disable' : 'Enable'}} Comments</a>
<a v-if="canEdit" class="dropdown-item font-weight-bold" :href="editUrl()">Edit</a>
<a class="dropdown-item font-weight-bold text-danger" v-on:click="deletePost(status)">Delete</a>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -108,30 +94,16 @@
</div> </div>
<div class="float-right"> <div class="float-right">
<div class="post-actions"> <div class="post-actions">
<div v-if="user != false" class="dropdown"> <div v-if="user != false">
<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"> <button class="btn btn-link text-dark no-caret" title="Post options" @click="ctxMenu()">
<span class="fas fa-ellipsis-v text-muted"></span> <span class="fas fa-ellipsis-v text-muted"></span>
</button> </button>
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownMenuButton">
<a class="dropdown-item font-weight-bold" @click="copyPostUrl()">Copy Post Url</a>
<a class="dropdown-item font-weight-bold" @click="showEmbedPostModal()">Embed</a>
<span v-if="!owner()">
<a class="dropdown-item font-weight-bold" :href="reportUrl()">Report</a>
<a class="dropdown-item font-weight-bold" v-on:click="muteProfile">Mute Profile</a>
<a class="dropdown-item font-weight-bold" v-on:click="blockProfile">Block Profile</a>
</span>
<span v-if="ownerOrAdmin()">
<a class="dropdown-item font-weight-bold" href="#" v-on:click.prevent="toggleCommentVisibility">{{ showComments ? 'Disable' : 'Enable'}} Comments</a>
<a v-if="canEdit" class="dropdown-item font-weight-bold" :href="editUrl()">Edit</a>
<a class="dropdown-item font-weight-bold text-danger" v-on:click="deletePost">Delete</a>
</span>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="d-flex flex-md-column flex-column-reverse h-100" style="overflow-y: auto;"> <div class="d-flex flex-md-column flex-column-reverse h-100" style="overflow-y: auto;">
<div class="card-body status-comments pb-5 pt-0"> <div class="card-body status-comments pt-0">
<div class="status-comment"> <div class="status-comment">
<div v-if="status.content.length" class="pt-3"> <div v-if="status.content.length" class="pt-3">
<div v-if="showCaption != true"> <div v-if="showCaption != true">
@ -227,7 +199,12 @@
</div> </div>
</div> </div>
<div class="card-body flex-grow-0 py-1"> <div v-if="reactionBarLoading" class="card-body flex-grow-0 py-4 text-center">
<div class="spinner-border" role="status">
<span class="sr-only">Loading...</span>
</div>
</div>
<div v-else class="card-body flex-grow-0 py-1">
<div v-if="loaded && user.hasOwnProperty('id')" class="reactions my-2 pb-1 d-flex justify-content-between"> <div v-if="loaded && user.hasOwnProperty('id')" class="reactions my-2 pb-1 d-flex justify-content-between">
<h3 v-bind:class="[reactions.liked ? 'fas fa-heart text-danger mr-3 m-0 cursor-pointer' : 'far fa-heart pr-3 m-0 like-btn cursor-pointer']" title="Like" v-on:click="likeStatus"></h3> <h3 v-bind:class="[reactions.liked ? 'fas fa-heart text-danger mr-3 m-0 cursor-pointer' : 'far fa-heart pr-3 m-0 like-btn cursor-pointer']" title="Like" v-on:click="likeStatus"></h3>
<h3 v-if="!status.comments_disabled" class="far fa-comment mr-3 m-0 cursor-pointer" title="Comment" v-on:click="replyFocus(status)"></h3> <h3 v-if="!status.comments_disabled" class="far fa-comment mr-3 m-0 cursor-pointer" title="Comment" v-on:click="replyFocus(status)"></h3>
@ -252,18 +229,13 @@
</div> </div>
</div> </div>
</div> </div>
<!-- <div v-if="showComments && user.length !== 0" class="card-footer bg-white px-2 py-0">
<ul class="nav align-items-center emoji-reactions" style="overflow-x: scroll;flex-wrap: unset;">
<li class="nav-item" v-on:click="emojiReaction" v-for="e in emoji">{{e}}</li>
</ul>
</div> -->
<div v-if="showComments" class="card-footer bg-white sticky-md-bottom p-0"> <div v-if="showComments" class="card-footer bg-white sticky-md-bottom p-0">
<div v-if="user.length == 0" class="comment-form-guest p-3"> <div v-if="user.length == 0" class="comment-form-guest p-3">
<a href="/login">Login</a> to like or comment. <a href="/login">Login</a> to like or comment.
</div> </div>
<form v-else class="border-0 rounded-0 align-middle" method="post" action="/i/comment" :data-id="statusId" data-truncate="false"> <form v-else class="border-0 rounded-0 align-middle" method="post" action="/i/comment" :data-id="statusId" data-truncate="false">
<textarea class="form-control border-0 rounded-0" name="comment" placeholder="Add a comment…" autocomplete="off" autocorrect="off" style="height:56px;line-height: 18px;max-height:80px;resize: none; padding-right:4.2rem;" v-model="replyText"></textarea> <textarea class="form-control border-0 rounded-0" name="comment" placeholder="Add a comment…" autocomplete="off" autocorrect="off" style="height:56px;line-height: 18px;max-height:80px;resize: none; padding-right:4.2rem;" @click="replyFocus(status)"></textarea>
<input type="button" value="Post" class="d-inline-block btn btn-link font-weight-bold reply-btn text-decoration-none" v-on:click.prevent="postReply" :disabled="replyText.length == 0" /> <input type="button" value="Post" class="d-inline-block btn btn-link font-weight-bold reply-btn text-decoration-none" disabled/>
</form> </form>
</div> </div>
</div> </div>
@ -271,9 +243,6 @@
</div> </div>
</div> </div>
<div class="container" v-if="showProfileMorePosts"> <div class="container" v-if="showProfileMorePosts">
<!-- <div class="py-4">
<hr>
</div> -->
<p class="text-lighter px-3 mt-5" style="font-weight: 600;font-size: 15px;">More posts from <a :href="'/'+statusUsername" class="text-dark">{{this.statusUsername}}</a></p> <p class="text-lighter px-3 mt-5" style="font-weight: 600;font-size: 15px;">More posts from <a :href="'/'+statusUsername" class="text-dark">{{this.statusUsername}}</a></p>
<div class="profile-timeline mt-md-4"> <div class="profile-timeline mt-md-4">
<div class="row"> <div class="row">
@ -474,6 +443,7 @@
</div> </div>
</div> </div>
</div> </div>
<b-modal ref="likesModal" <b-modal ref="likesModal"
id="l-modal" id="l-modal"
hide-footer hide-footer
@ -512,9 +482,9 @@
hide-footer hide-footer
centered centered
title="Shares" title="Shares"
body-class="list-group-flush p-0"> body-class="list-group-flush py-3 px-0">
<div class="list-group"> <div class="list-group">
<div class="list-group-item border-0" v-for="(user, index) in shares" :key="'modal_shares_'+index"> <div class="list-group-item border-0 py-1" v-for="(user, index) in shares" :key="'modal_shares_'+index">
<div class="media"> <div class="media">
<a :href="user.url"> <a :href="user.url">
<img class="mr-3 rounded-circle box-shadow" :src="user.avatar" :alt="user.username + 's avatar'" width="30px"> <img class="mr-3 rounded-circle box-shadow" :src="user.avatar" :alt="user.username + 's avatar'" width="30px">
@ -615,6 +585,88 @@
</div> </div>
<p class="mb-0 text-center small text-muted font-weight-bold"><a href="/site/kb/tagging-people">Learn more</a> about Tagging People.</p> <p class="mb-0 text-center small text-muted font-weight-bold"><a href="/site/kb/tagging-people">Learn more</a> about Tagging People.</p>
</b-modal> </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="user && user.id != status.account.id && relationship && relationship.following" class="list-group-item rounded cursor-pointer font-weight-bold text-danger" @click="ctxMenuUnfollow()">Unfollow</div>
<div v-if="user && user.id != status.account.id && relationship && !relationship.following" class="list-group-item rounded cursor-pointer font-weight-bold text-primary" @click="ctxMenuFollow()">Follow</div>
<div v-if="status && status.local == true" class="list-group-item rounded cursor-pointer" @click="showEmbedPostModal()">Embed</div>
<div class="list-group-item rounded cursor-pointer" @click="ctxMenuCopyLink()">Copy Link</div>
<div v-if="status && user.id == status.account.id" class="list-group-item rounded cursor-pointer" @click="toggleCommentVisibility">{{ showComments ? 'Disable' : 'Enable'}} Comments</div>
<a v-if="status && user.id == status.account.id" class="list-group-item rounded cursor-pointer text-dark text-decoration-none" :href="editUrl()">Edit</a>
<div v-if="user && user.is_admin == true" class="list-group-item rounded cursor-pointer" @click="ctxModMenu()">ModTools</div>
<div v-if="status && user.id != status.account.id && !relationship.blocking && !user.is_admin" class="list-group-item rounded cursor-pointer font-weight-bold text-danger" @click="blockProfile()">Block</div>
<div v-if="status && user.id != status.account.id && relationship.blocking && !user.is_admin" class="list-group-item rounded cursor-pointer font-weight-bold text-danger" @click="unblockProfile()">Unblock</div>
<a v-if="user && user.id != status.account.id && !user.is_admin" class="list-group-item rounded cursor-pointer font-weight-bold text-danger text-decoration-none" :href="reportUrl()">Report</a>
<div v-if="status && (user.is_admin || user.id == status.account.id)" class="list-group-item rounded cursor-pointer font-weight-bold text-danger" @click="deletePost(ctxMenuStatus)">Delete</div>
<div class="list-group-item rounded cursor-pointer text-lighter" @click="closeCtxMenu()">Cancel</div>
</div>
</b-modal>
<b-modal ref="ctxModModal"
id="ctx-mod-modal"
hide-header
hide-footer
centered
rounded
size="sm"
body-class="list-group-flush p-0 rounded">
<div class="list-group text-center">
<div class="list-group-item rounded cursor-pointer" @click="toggleCommentVisibility">{{ showComments ? 'Disable' : 'Enable'}} Comments</div>
<div class="list-group-item rounded cursor-pointer" @click="moderatePost('unlist')">Unlist from Timelines</div>
<div v-if="status.sensitive" class="list-group-item rounded cursor-pointer" @click="moderatePost('remcw')">Remove Content Warning</div>
<div v-else class="list-group-item rounded cursor-pointer" @click="moderatePost('addcw')">Add Content Warning</div>
<div class="list-group-item rounded cursor-pointer text-lighter" @click="ctxModMenuClose()">Cancel</div>
</div>
</b-modal>
<b-modal ref="replyModal"
id="ctx-reply-modal"
hide-footer
centered
rounded
:title-html="replyingToUsername ? 'Reply to <span class=text-dark>' + replyingToUsername + '</span>' : ''"
title-tag="p"
title-class="font-weight-bold text-muted"
size="md"
body-class="p-2 rounded">
<div>
<textarea class="form-control" rows="4" style="border: none; font-size: 18px; resize: none; white-space: pre-wrap;outline: none;" placeholder="Reply here ..." v-model="replyText">
</textarea>
<div class="border-top border-bottom my-2">
<ul class="nav align-items-center emoji-reactions" style="overflow-x: scroll;flex-wrap: unset;">
<li class="nav-item" v-on:click="emojiReaction(status)" v-for="e in emoji">{{e}}</li>
</ul>
</div>
<div class="d-flex justify-content-between align-items-center">
<div>
<span class="pl-2 small text-muted font-weight-bold text-monospace">
<span :class="[replyText.length > config.uploader.max_caption_length ? 'text-danger':'text-dark']">{{replyText.length > config.uploader.max_caption_length ? config.uploader.max_caption_length - replyText.length : replyText.length}}</span>/{{config.uploader.max_caption_length}}
</span>
</div>
<div class="d-flex align-items-center">
<div class="custom-control custom-switch mr-3">
<input type="checkbox" class="custom-control-input" id="replyModalCWSwitch" v-model="replySensitive">
<label :class="[replySensitive ? 'custom-control-label font-weight-bold text-dark':'custom-control-label text-lighter']" for="replyModalCWSwitch">Mark as NSFW</label>
</div>
<!-- <select class="custom-select custom-select-sm my-0 mr-2">
<option value="public" selected="">Public</option>
<option value="unlisted">Unlisted</option>
<option value="followers">Followers Only</option>
</select> -->
<button class="btn btn-primary btn-sm py-2 px-4 lead text-uppercase font-weight-bold" v-on:click.prevent="postReply()" :disabled="replyText.length == 0">
{{replySending == true ? 'POSTING' : 'POST'}}
</button>
</div>
</div>
</div>
</b-modal>
</div> </div>
</template> </template>
@ -742,6 +794,7 @@ export default {
loaded: false, loaded: false,
loading: null, loading: null,
replyingToId: this.statusId, replyingToId: this.statusId,
replyingToUsername: this.statusUsername,
replyToIndex: 0, replyToIndex: 0,
replySending: false, replySending: false,
emoji: window.App.util.emoji, emoji: window.App.util.emoji,
@ -753,9 +806,10 @@ export default {
ctxEmbedShowLikes: false, ctxEmbedShowLikes: false,
ctxEmbedCompactMode: false, ctxEmbedCompactMode: false,
layout: this.profileLayout, layout: this.profileLayout,
canEdit: false,
showProfileMorePosts: false, showProfileMorePosts: false,
profileMorePosts: [] profileMorePosts: [],
replySending: false,
reactionBarLoading: true,
} }
}, },
watch: { watch: {
@ -811,16 +865,6 @@ export default {
}, },
methods: { methods: {
showMuteBlock() {
let sid = this.status.account.id;
let uid = this.user.id;
if(sid == uid) {
$('.post-actions .menu-author').removeClass('d-none');
} else {
$('.post-actions .menu-user').removeClass('d-none');
}
},
reportUrl() { reportUrl() {
return '/i/report?type=post&id=' + this.status.id; return '/i/report?type=post&id=' + this.status.id;
}, },
@ -839,33 +883,20 @@ export default {
axios.get('/api/v2/profile/'+this.statusUsername+'/status/'+this.statusId) axios.get('/api/v2/profile/'+this.statusUsername+'/status/'+this.statusId)
.then(response => { .then(response => {
self.status = response.data.status; self.status = response.data.status;
self.user = response.data.user;
window._sharedData.curUser = self.user;
window.App.util.navatar();
self.media = self.status.media_attachments; self.media = self.status.media_attachments;
self.reactions = response.data.reactions;
self.likes = response.data.likes;
self.shares = response.data.shares;
self.likesPage = 2; self.likesPage = 2;
self.sharesPage = 2; self.sharesPage = 2;
this.showMuteBlock();
self.showCaption = !response.data.status.sensitive; self.showCaption = !response.data.status.sensitive;
if(self.status.comments_disabled == false) { if(self.status.comments_disabled == false) {
self.showComments = true; self.showComments = true;
this.fetchComments(); this.fetchComments();
} }
if(this.ownerOrAdmin()) {
let od = new Date(this.status.created_at).getTime() + (1 * 24 * 60 * 60 * 1000);
let now = new Date().getTime();
if(od > now) {
this.canEdit = true;
}
}
this.loaded = true; this.loaded = true;
setTimeout(function() { setTimeout(function() {
self.fetchProfilePosts(); self.fetchProfilePosts();
}, 3000); }, 3000);
setTimeout(function() { setTimeout(function() {
self.fetchState();
document.querySelectorAll('.status-comment .comment-text a').forEach(function(i, e) { document.querySelectorAll('.status-comment .comment-text a').forEach(function(i, e) {
if(i.href.startsWith(window.location.origin)) { if(i.href.startsWith(window.location.origin)) {
return; return;
@ -882,6 +913,20 @@ export default {
}); });
}, },
fetchState() {
let self = this;
axios.get('/api/v2/profile/'+this.statusUsername+'/status/'+this.statusId+'/state')
.then(res => {
self.user = res.data.user;
window._sharedData.curUser = self.user;
window.App.util.navatar();
self.likes = res.data.likes;
self.shares = res.data.shares;
self.reactions = res.data.reactions;
self.reactionBarLoading = false;
});
},
likesModal() { likesModal() {
if($('body').hasClass('loggedIn') == false) { if($('body').hasClass('loggedIn') == false) {
window.location.href = '/login?next=' + encodeURIComponent('/p/' + this.status.shortcode); window.location.href = '/login?next=' + encodeURIComponent('/p/' + this.status.shortcode);
@ -890,14 +935,31 @@ export default {
if(this.status.favourites_count == 0) { if(this.status.favourites_count == 0) {
return; return;
} }
this.$refs.likesModal.show(); if(this.likes.length) {
this.$refs.likesModal.show();
return;
}
axios.get('/api/v2/likes/profile/'+this.statusUsername+'/status/'+this.statusId)
.then(res => {
this.likes = res.data.data;
this.$refs.likesModal.show();
});
}, },
sharesModal() { sharesModal() {
if(this.status.reblogs_count == 0 || $('body').hasClass('loggedIn') == false) { if(this.status.reblogs_count == 0 || $('body').hasClass('loggedIn') == false) {
window.location.href = '/login?next=' + encodeURIComponent('/p/' + this.status.shortcode);
return; return;
} }
this.$refs.sharesModal.show(); if(this.shares.length) {
this.$refs.sharesModal.show();
return;
}
axios.get('/api/v2/shares/profile/'+this.statusUsername+'/status/'+this.statusId)
.then(res => {
this.shares = res.data.data;
this.$refs.sharesModal.show();
});
}, },
infiniteLikesHandler($state) { infiniteLikesHandler($state) {
@ -1010,21 +1072,6 @@ export default {
}); });
}, },
muteProfile() {
if($('body').hasClass('loggedIn') == false) {
return;
}
axios.post('/i/mute', {
type: 'user',
item: this.status.account.id
}).then(res => {
swal('Success', 'You have successfully muted ' + this.status.account.acct, 'success');
}).catch(err => {
swal('Error', 'Something went wrong. Please try again later.', 'error');
});
},
blockProfile() { blockProfile() {
if($('body').hasClass('loggedIn') == false) { if($('body').hasClass('loggedIn') == false) {
return; return;
@ -1034,12 +1081,31 @@ export default {
type: 'user', type: 'user',
item: this.status.account.id item: this.status.account.id
}).then(res => { }).then(res => {
this.$refs.ctxModal.hide();
this.relationship.blocking = true;
swal('Success', 'You have successfully blocked ' + this.status.account.acct, 'success'); swal('Success', 'You have successfully blocked ' + this.status.account.acct, 'success');
}).catch(err => { }).catch(err => {
swal('Error', 'Something went wrong. Please try again later.', 'error'); swal('Error', 'Something went wrong. Please try again later.', 'error');
}); });
}, },
unblockProfile() {
if($('body').hasClass('loggedIn') == false) {
return;
}
axios.post('/i/unblock', {
type: 'user',
item: this.status.account.id
}).then(res => {
this.relationship.blocking = false;
this.$refs.ctxModal.hide();
swal('Success', 'You have successfully unblocked ' + this.status.account.acct, 'success');
}).catch(err => {
swal('Error', 'Something went wrong. Please try again later.', 'error');
});
},
deletePost(status) { deletePost(status) {
if(!this.ownerOrAdmin()) { if(!this.ownerOrAdmin()) {
return; return;
@ -1082,6 +1148,7 @@ export default {
postReply() { postReply() {
let self = this; let self = this;
this.replySending = true;
if(this.replyText.length == 0 || if(this.replyText.length == 0 ||
this.replyText.trim() == '@'+this.status.account.acct) { this.replyText.trim() == '@'+this.status.account.acct) {
self.replyText = null; self.replyText = null;
@ -1106,7 +1173,7 @@ export default {
self.results.unshift(entity); self.results.unshift(entity);
} }
let elem = $('.status-comments')[0]; let elem = $('.status-comments')[0];
elem.scrollTop = elem.clientHeight; elem.scrollTop = elem.clientHeight * 2;
} else { } else {
if(self.replyToIndex >= 0) { if(self.replyToIndex >= 0) {
let el = self.results[self.replyToIndex]; let el = self.results[self.replyToIndex];
@ -1114,6 +1181,8 @@ export default {
el.reply_count = el.reply_count + 1; el.reply_count = el.reply_count + 1;
} }
} }
self.$refs.replyModal.hide();
self.replySending = false;
}); });
}, },
@ -1147,15 +1216,25 @@ export default {
}, },
replyFocus(e, index, prependUsername = false) { replyFocus(e, index, prependUsername = false) {
if($('body').hasClass('loggedIn') == false) {
this.redirect('/login?next=' + encodeURIComponent(window.location.pathname));
return;
}
if(this.status.comments_disabled) {
return;
}
this.replyToIndex = index; this.replyToIndex = index;
this.replyingToId = e.id; this.replyingToId = e.id;
this.replyingToUsername = e.account.username;
this.reply_to_profile_id = e.account.id; this.reply_to_profile_id = e.account.id;
let username = e.account.local ? '@' + e.account.username + ' ' let username = e.account.local ? '@' + e.account.username + ' '
: '@' + e.account.acct + ' '; : '@' + e.account.acct + ' ';
if(prependUsername == true) { if(prependUsername == true) {
this.replyText = username; this.replyText = username;
} }
$('textarea[name="comment"]').focus(); this.$refs.replyModal.show();
}, },
fetchComments() { fetchComments() {
@ -1289,7 +1368,9 @@ export default {
item: self.status.id, item: self.status.id,
disableComments: false disableComments: false
}).then(function(res) { }).then(function(res) {
window.location.href = self.status.url; self.status.comments_disabled = false;
self.$refs.ctxModal.hide();
window.location.reload();
}).catch(function(err) { }).catch(function(err) {
return; return;
}); });
@ -1299,8 +1380,9 @@ export default {
item: self.status.id, item: self.status.id,
disableComments: true disableComments: true
}).then(function(res) { }).then(function(res) {
self.status.comments_disabled = false; self.status.comments_disabled = true;
self.showComments = false; self.showComments = false;
self.$refs.ctxModal.hide();
}).catch(function(err) { }).catch(function(err) {
return; return;
}); });
@ -1374,6 +1456,7 @@ export default {
showEmbedPostModal() { showEmbedPostModal() {
let mode = this.ctxEmbedCompactMode ? 'compact' : 'full'; let mode = this.ctxEmbedCompactMode ? 'compact' : 'full';
this.ctxEmbedPayload = window.App.util.embed.post(this.status.url, this.ctxEmbedShowCaption, this.ctxEmbedShowLikes, mode); this.ctxEmbedPayload = window.App.util.embed.post(this.status.url, this.ctxEmbedShowCaption, this.ctxEmbedShowLikes, mode);
this.$refs.ctxModal.hide();
this.$refs.embedModal.show(); this.$refs.embedModal.show();
}, },
@ -1461,10 +1544,166 @@ export default {
swal('An Error Occurred', 'Please try again later.', 'error'); swal('An Error Occurred', 'Please try again later.', 'error');
}); });
}, },
copyPostUrl() { copyPostUrl() {
navigator.clipboard.writeText(this.statusUrl); navigator.clipboard.writeText(this.statusUrl);
return; return;
} },
moderatePost(action, $event) {
let status = this.status;
let username = status.account.username;
let msg = '';
let self = this;
switch(action) {
case 'addcw':
msg = 'Are you sure you want to add a content warning to this post?';
swal({
title: 'Confirm',
text: msg,
icon: 'warning',
buttons: true,
dangerMode: true
}).then(res => {
if(res) {
axios.post('/api/v2/moderator/action', {
action: action,
item_id: status.id,
item_type: 'status'
}).then(res => {
swal('Success', 'Successfully added content warning', 'success');
status.sensitive = true;
self.ctxModMenuClose();
}).catch(err => {
swal(
'Error',
'Something went wrong, please try again later.',
'error'
);
self.ctxModMenuClose();
});
}
});
break;
case 'remcw':
msg = 'Are you sure you want to remove the content warning on this post?';
swal({
title: 'Confirm',
text: msg,
icon: 'warning',
buttons: true,
dangerMode: true
}).then(res => {
if(res) {
axios.post('/api/v2/moderator/action', {
action: action,
item_id: status.id,
item_type: 'status'
}).then(res => {
swal('Success', 'Successfully added content warning', 'success');
status.sensitive = false;
self.ctxModMenuClose();
}).catch(err => {
swal(
'Error',
'Something went wrong, please try again later.',
'error'
);
self.ctxModMenuClose();
});
}
});
break;
case 'unlist':
msg = 'Are you sure you want to unlist this post?';
swal({
title: 'Confirm',
text: msg,
icon: 'warning',
buttons: true,
dangerMode: true
}).then(res => {
if(res) {
axios.post('/api/v2/moderator/action', {
action: action,
item_id: status.id,
item_type: 'status'
}).then(res => {
// this.feed = this.feed.filter(f => {
// return f.id != status.id;
// });
swal('Success', 'Successfully unlisted post', 'success');
self.ctxModMenuClose();
}).catch(err => {
self.ctxModMenuClose();
swal(
'Error',
'Something went wrong, please try again later.',
'error'
);
});
}
});
break;
}
},
ctxMenu() {
this.$refs.ctxModal.show();
return;
},
closeCtxMenu(truncate) {
this.$refs.ctxModal.hide();
},
ctxModMenu() {
this.$refs.ctxModal.hide();
this.$refs.ctxModModal.show();
},
ctxModMenuClose() {
this.$refs.ctxModal.hide();
this.$refs.ctxModModal.hide();
},
ctxMenuCopyLink() {
let status = this.status;
navigator.clipboard.writeText(status.url);
this.closeCtxMenu();
return;
},
ctxMenuFollow() {
let id = this.status.account.id;
axios.post('/i/follow', {
item: id
}).then(res => {
let username = this.status.account.acct;
this.relationship.following = true;
this.$refs.ctxModal.hide();
setTimeout(function() {
swal('Follow successful!', 'You are now following ' + username, 'success');
}, 500);
});
},
ctxMenuUnfollow() {
let id = this.status.account.id;
axios.post('/i/follow', {
item: id
}).then(res => {
let username = this.status.account.acct;
this.relationship.following = false;
this.$refs.ctxModal.hide();
setTimeout(function() {
swal('Unfollow successful!', 'You are no longer following ' + username, 'success');
}, 500);
});
},
}, },
} }
</script> </script>

View file

@ -57,35 +57,8 @@
<!-- a class="list-group-item font-weight-bold text-decoration-none" :href="status.url">Share</a> <!-- a class="list-group-item font-weight-bold text-decoration-none" :href="status.url">Share</a>
<a class="list-group-item font-weight-bold text-decoration-none" :href="status.url">Embed</a> --> <a class="list-group-item font-weight-bold text-decoration-none" :href="status.url">Embed</a> -->
<a class="list-group-item text-dark text-decoration-none" href="#" @click.prevent="hidePost(status)">Hide</a> <a class="list-group-item text-dark text-decoration-none" href="#" @click.prevent="hidePost(status)">Hide</a>
<a v-if="activeSession == true && !statusOwner(status)" class="list-group-item text-dark text-decoration-none" :href="reportUrl(status)">Report</a> <a v-if="activeSession == true && !statusOwner(status)" class="list-group-item text-danger font-weight-bold text-decoration-none" :href="reportUrl(status)">Report</a>
<a v-if="activeSession == true && !statusOwner(status)" class="list-group-item text-dark text-decoration-none" @click.prevent="muteProfile(status)" href="#">Mute Profile</a> <div v-if="activeSession == true && statusOwner(status) == true || profile.is_admin == true" class="list-group-item text-danger font-weight-bold cursor-pointer" @click.prevent="deletePost">Delete</div>
<a v-if="activeSession == true && !statusOwner(status)" class="list-group-item text-dark text-decoration-none" @click.prevent="blockProfile(status)" href="#">Block Profile</a>
<span v-if="activeSession == true && statusOwner(status) == true || profile.is_admin == true">
<a class="list-group-item text-danger text-decoration-none" @click.prevent="deletePost">Delete</a>
</span>
<span v-if="activeSession == true && profile.is_admin == true">
<a class="list-group-item text-dark text-decoration-none" v-on:click="moderatePost(status, 'autocw')" href="#">
<p class="mb-0">Enforce CW</p>
<p class="mb-0 small text-muted">Adds a CW to every post <br> made by this account.</p>
</a>
<a class="list-group-item text-dark text-decoration-none" v-on:click="moderatePost(status, 'noautolink')" href="#">
<p class="mb-0">No Autolinking</p>
<p class="mb-0 small text-muted">Do not transform mentions, <br> hashtags or urls into HTML.</p>
</a>
<a class="list-group-item text-dark text-decoration-none" v-on:click="moderatePost(status, 'unlisted')" href="#">
<p class="mb-0">Unlisted Posts</p>
<p class="mb-0 small text-muted">Removes account from <br> public/network timelines.</p>
</a>
<a class="list-group-item text-dark text-decoration-none" v-on:click="moderatePost(status, 'disable')" href="#">
<p class="mb-0">Disable Account</p>
<p class="mb-0 small text-muted">Temporarily disable account <br> until next time user log in.</p>
</a>
<a class="list-group-item text-dark text-decoration-none" v-on:click="moderatePost(status, 'suspend')" href="#">
<p class="mb-0">Suspend Account</p>
<p class="mb-0 small text-muted">This prevents any new interactions, <br> without deleting existing data.</p>
</a>
</span>
</div> </div>
</div> </div>
</div> </div>

View file

@ -438,11 +438,11 @@
size="sm" size="sm"
body-class="list-group-flush p-0 rounded"> body-class="list-group-flush p-0 rounded">
<div class="list-group text-center"> <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="ctxMenuStatus && ctxMenuStatus.account.id != profile.id" class="list-group-item rounded cursor-pointer font-weight-bold text-danger" @click="ctxMenuReportPost()">Report</div>
<div v-if="ctxMenuStatus && ctxMenuStatus.account.id != profile.id && ctxMenuRelationship && ctxMenuRelationship.following" class="list-group-item rounded cursor-pointer font-weight-bold text-danger" @click="ctxMenuUnfollow()">Unfollow</div> <div v-if="ctxMenuStatus && ctxMenuStatus.account.id != profile.id && ctxMenuRelationship && ctxMenuRelationship.following" class="list-group-item rounded cursor-pointer font-weight-bold text-danger" @click="ctxMenuUnfollow()">Unfollow</div>
<div v-if="ctxMenuStatus && ctxMenuStatus.account.id != profile.id && ctxMenuRelationship && !ctxMenuRelationship.following" class="list-group-item rounded cursor-pointer font-weight-bold text-primary" @click="ctxMenuFollow()">Follow</div> <div v-if="ctxMenuStatus && ctxMenuStatus.account.id != profile.id && 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="ctxMenuGoToPost()">Go to post</div>
<div v-if="ctxMenuStatus && ctxMenuStatus.local == true" class="list-group-item rounded cursor-pointer" @click="ctxMenuEmbed()">Embed</div> <div v-if="ctxMenuStatus && ctxMenuStatus.local == true && !ctxMenuStatus.in_reply_to_id" 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="ctxMenuShare()">Share</div> -->
<div class="list-group-item rounded cursor-pointer" @click="ctxMenuCopyLink()">Copy Link</div> <div class="list-group-item rounded cursor-pointer" @click="ctxMenuCopyLink()">Copy Link</div>
<div v-if="profile && profile.is_admin == true" class="list-group-item rounded cursor-pointer" @click="ctxModMenuShow()">Moderation Tools</div> <div v-if="profile && profile.is_admin == true" class="list-group-item rounded cursor-pointer" @click="ctxModMenuShow()">Moderation Tools</div>
@ -558,6 +558,10 @@
</span> </span>
</div> </div>
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
<div class="custom-control custom-switch mr-3">
<input type="checkbox" class="custom-control-input" id="replyModalCWSwitch" v-model="replyNsfw">
<label :class="[replyNsfw ? 'custom-control-label font-weight-bold text-dark':'custom-control-label text-lighter']" for="replyModalCWSwitch">Mark as NSFW</label>
</div>
<!-- <select class="custom-select custom-select-sm my-0 mr-2"> <!-- <select class="custom-select custom-select-sm my-0 mr-2">
<option value="public" selected="">Public</option> <option value="public" selected="">Public</option>
<option value="unlisted">Unlisted</option> <option value="unlisted">Unlisted</option>
@ -675,6 +679,7 @@
showReadMore: true, showReadMore: true,
replyStatus: {}, replyStatus: {},
replyText: '', replyText: '',
replyNsfw: false,
emoji: window.App.util.emoji, emoji: window.App.util.emoji,
showHashtagPosts: false, showHashtagPosts: false,
hashtagPosts: [], hashtagPosts: [],
@ -697,6 +702,7 @@
mpPoller: null mpPoller: null
} }
}, },
watch: { watch: {
ctxEmbedShowCaption: function (n,o) { ctxEmbedShowCaption: function (n,o) {
if(n == true) { if(n == true) {
@ -721,6 +727,7 @@
this.ctxEmbedPayload = window.App.util.embed.post(this.ctxMenuStatus.url, this.ctxEmbedShowCaption, this.ctxEmbedShowLikes, mode); this.ctxEmbedPayload = window.App.util.embed.post(this.ctxMenuStatus.url, this.ctxEmbedShowCaption, this.ctxEmbedShowLikes, mode);
} }
}, },
beforeMount() { beforeMount() {
this.fetchProfile(); this.fetchProfile();
this.fetchTimelineApi(); this.fetchTimelineApi();
@ -1072,7 +1079,8 @@
} }
axios.post('/i/comment', { axios.post('/i/comment', {
item: id, item: id,
comment: comment comment: comment,
sensitive: this.replyNsfw
}).then(res => { }).then(res => {
this.replyText = ''; this.replyText = '';
this.replies.unshift(res.data.entity); this.replies.unshift(res.data.entity);
@ -1663,6 +1671,7 @@
}, 500); }, 500);
}, },
}, },
beforeDestroy () { beforeDestroy () {
clearInterval(this.mpInterval); clearInterval(this.mpInterval);
}, },

View file

@ -120,6 +120,7 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact
Route::get('discover', 'InternalApiController@discover'); Route::get('discover', 'InternalApiController@discover');
Route::get('discover/posts', 'InternalApiController@discoverPosts')->middleware('auth:api'); Route::get('discover/posts', 'InternalApiController@discoverPosts')->middleware('auth:api');
Route::get('profile/{username}/status/{postid}', 'PublicApiController@status'); Route::get('profile/{username}/status/{postid}', 'PublicApiController@status');
Route::get('profile/{username}/status/{postid}/state', 'PublicApiController@statusState');
Route::get('comments/{username}/status/{postId}', 'PublicApiController@statusComments'); Route::get('comments/{username}/status/{postId}', 'PublicApiController@statusComments');
Route::get('likes/profile/{username}/status/{id}', 'PublicApiController@statusLikes'); Route::get('likes/profile/{username}/status/{id}', 'PublicApiController@statusLikes');
Route::get('shares/profile/{username}/status/{id}', 'PublicApiController@statusShares'); Route::get('shares/profile/{username}/status/{id}', 'PublicApiController@statusShares');