Add partial components

This commit is contained in:
Daniel Supernault 2023-06-11 15:23:23 -06:00
parent 5361082026
commit f5dbc8281a
No known key found for this signature in database
GPG key ID: 0DEF1C662C9033F7
6 changed files with 1747 additions and 0 deletions

View file

@ -0,0 +1,19 @@
<template>
<div class="ph-item border-0 shadow-sm" style="border-radius:15px;margin-bottom: 1rem;">
<div class="ph-col-12">
<div class="ph-row align-items-center">
<div class="ph-avatar mr-3 d-flex" style="width:50px;height:60px;border-radius: 15px;"></div>
<div class="ph-col-6 big"></div>
</div>
<div class="empty"></div>
<div class="empty"></div>
<div class="ph-picture"></div>
<div class="ph-row">
<div class="ph-col-12 empty"></div>
<div class="ph-col-12 big"></div>
<div class="ph-col-12 empty"></div>
<div class="ph-col-12"></div>
</div>
</div>
</div>
</template>

View file

@ -0,0 +1,468 @@
<template>
<div class="timeline-status-component">
<div class="card shadow-sm" style="border-radius: 15px;">
<post-header
:profile="profile"
:status="status"
@menu="openMenu"
@follow="follow"
@unfollow="unfollow" />
<post-content
:profile="profile"
:status="status" />
<post-reactions
v-if="reactionBar"
:status="status"
:profile="profile"
:admin="admin"
v-on:like="like"
v-on:unlike="unlike"
v-on:share="shareStatus"
v-on:unshare="unshareStatus"
v-on:likes-modal="showLikes"
v-on:shares-modal="showShares"
v-on:toggle-comments="showComments"
v-on:bookmark="handleBookmark"
v-on:mod-tools="openModTools" />
<div v-if="showCommentDrawer" class="card-footer rounded-bottom border-0" style="background: rgba(0,0,0,0.02);z-index: 3;">
<comment-drawer
:status="status"
:profile="profile"
v-on:handle-report="handleReport"
v-on:counter-change="counterChange"
v-on:show-likes="showCommentLikes"
v-on:follow="follow"
v-on:unfollow="unfollow" />
</div>
</div>
</div>
</template>
<script type="text/javascript">
import CommentDrawer from './post/CommentDrawer.vue';
import PostHeader from './post/PostHeader.vue';
import PostContent from './post/PostContent.vue';
import PostReactions from './post/PostReactions.vue';
export default {
props: {
status: {
type: Object
},
profile: {
type: Object
},
reactionBar: {
type: Boolean,
default: true
},
useDropdownMenu: {
type: Boolean,
default: false
}
},
components: {
"comment-drawer": CommentDrawer,
"post-content": PostContent,
"post-header": PostHeader,
"post-reactions": PostReactions
},
data() {
return {
key: 1,
menuLoading: true,
sensitive: false,
showCommentDrawer: false,
isReblogging: false,
isBookmarking: false,
owner: false,
admin: false,
license: false
}
},
mounted() {
this.license = this.status.media_attachments && this.status.media_attachments.length ?
this.status
.media_attachments
.filter(m => m.hasOwnProperty('license') && m.license && m.license.hasOwnProperty('id'))
.map(m => m.license)[0] : false;
this.admin = window._sharedData.user.is_admin;
this.owner = this.status.account.id == window._sharedData.user.id;
if(this.status.reply_count && this.autoloadComments && this.status.comments_disabled === false) {
setTimeout(() => {
this.showCommentDrawer = true;
}, 1000);
}
},
computed: {
hideCounts: {
get() {
return this.$store.state.hideCounts == true;
}
},
fixedHeight: {
get() {
return this.$store.state.fixedHeight == true;
}
},
autoloadComments: {
get() {
return this.$store.state.autoloadComments == true;
}
},
newReactions: {
get() {
return this.$store.state.newReactions;
},
}
},
watch: {
status: {
deep: true,
immediate: true,
handler: function(o, n) {
this.isBookmarking = false;
}
}
},
methods: {
openMenu() {
this.$emit('menu');
},
like() {
this.$emit('like');
},
unlike() {
this.$emit('unlike');
},
showLikes() {
this.$emit('likes-modal');
},
showShares() {
this.$emit('shares-modal');
},
showComments() {
this.showCommentDrawer = !this.showCommentDrawer;
},
copyLink() {
event.currentTarget.blur();
App.util.clipboard(this.status.url);
},
shareToOther() {
if (navigator.canShare) {
navigator.share({
url: this.status.url
})
.then(() => console.log('Share was successful.'))
.catch((error) => console.log('Sharing failed', error));
} else {
swal('Not supported', 'Your current device does not support native sharing.', 'error');
}
},
counterChange(type) {
this.$emit('counter-change', type);
},
showCommentLikes(post) {
this.$emit('comment-likes-modal', post);
},
shareStatus() {
this.$emit('share');
},
unshareStatus() {
this.$emit('unshare');
},
handleReport(post) {
this.$emit('handle-report', post);
},
follow() {
this.$emit('follow');
},
unfollow() {
this.$emit('unfollow');
},
handleReblog() {
this.isReblogging = true;
if(this.status.reblogged) {
this.$emit('unshare');
} else {
this.$emit('share');
}
setTimeout(() => {
this.isReblogging = false;
}, 5000);
},
handleBookmark() {
event.currentTarget.blur();
this.isBookmarking = true;
this.$emit('bookmark');
setTimeout(() => {
this.isBookmarking = false;
}, 5000);
},
getStatusAvatar() {
if(window._sharedData.user.id == this.status.account.id) {
return window._sharedData.user.avatar;
}
return this.status.account.avatar;
},
openModTools() {
this.$emit('mod-tools');
}
}
}
</script>
<style lang="scss">
.timeline-status-component {
margin-bottom: 1rem;
.btn:focus {
box-shadow: none !important;
}
.avatar {
border-radius: 15px;
}
.VueCarousel-wrapper {
.VueCarousel-slide {
img {
object-fit: contain;
}
}
}
.status-text {
z-index: 3;
&.py-0 {
font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
}
}
.reaction-liked-by {
font-size: 11px;
font-weight: 600;
font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
}
.timestamp,
.visibility,
.location {
color: #94a3b8;
font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
}
.invisible {
display: none;
}
.blurhash-wrapper {
img {
border-radius:0;
object-fit: cover;
}
canvas {
border-radius: 0;
}
}
.content-label-wrapper {
position: relative;
width: 100%;
height: 400px;
background-color: #000;
border-radius: 0;
overflow: hidden;
img, canvas {
max-height: 400px;
cursor: pointer;
}
}
.content-label {
margin: 0;
position: absolute;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
z-index: 2;
border-radius: 0;
background: rgba(0, 0, 0, 0.2)
}
.rounded-bottom {
border-bottom-left-radius: 15px !important;
border-bottom-right-radius: 15px !important;
}
.card-footer {
.media {
position: relative;
.comment-border-link {
display: block;
position: absolute;
top: 40px;
left: 11px;
width: 10px;
height: calc(100% - 100px);
border-left: 4px solid transparent;
border-right: 4px solid transparent;
background-color: #E5E7EB;
background-clip: padding-box;
&:hover {
background-color: #BFDBFE;
}
}
.child-reply-form {
position: relative;
}
.comment-border-arrow {
display: block;
position: absolute;
top: -6px;
left: -33px;
width: 10px;
height: 29px;
border-left: 4px solid transparent;
border-right: 4px solid transparent;
background-color: #E5E7EB;
background-clip: padding-box;
border-bottom: 2px solid transparent;
&:after {
content: '';
display: block;
position: absolute;
top: 25px;
left: 2px;
width: 15px;
height: 2px;
background-color: #E5E7EB;
}
}
&-status {
margin-bottom: 1.3rem;
}
&-avatar {
margin-right: 12px;
border-radius: 8px;
}
&-body {
&-comment {
width: fit-content;
padding: 0.4rem 0.7rem;
background-color: var(--comment-bg);
border-radius: 0.9rem;
&-username {
margin-bottom: 0.25rem !important;
font-size: 14px;
font-weight: 700 !important;
color: var(--body-color);
a {
color: var(--body-color);
text-decoration: none;
}
}
&-content {
margin-bottom: 0;
font-size: 16px;
}
}
&-reactions {
margin-top: 0.4rem !important;
margin-bottom: 0 !important;
color: #B8C2CC !important;
font-size: 12px;
}
}
}
}
.fixedHeight {
max-height: 400px;
.VueCarousel-wrapper {
border-radius: 15px;
}
.VueCarousel-slide {
img {
max-height: 400px;
}
}
.blurhash-wrapper {
img {
height: 400px;
max-height: 400px;
background-color: transparent;
object-fit: contain;
}
canvas {
max-height: 400px;
}
}
.content-label-wrapper {
border-radius: 15px;
}
.content-label {
height: 400px;
border-radius: 0;
}
}
}
</style>

View file

@ -0,0 +1,100 @@
<template>
<div>
<!-- <p class="">
<router-link class="btn btn-primary primary btn-sm rounded-pill font-weight-bold btn-block" to="/i/web/whats-new"><i class="fal fa-exclamation-circle mr-1"></i> New in Metro UI 2</router-link>
</p> -->
<notifications :profile="profile" />
<!-- <div class="d-none card shadow-sm mb-3" style="border-radius: 15px;">
<div class="card-body">
<div class="d-flex justify-content-between">
<p class="text-muted">{{ $t('timeline.peopleYouMayKnow') }}</p>
<p class="text-lighter"><i class="far fa-cog"></i></p>
</div>
<div class="media-list mb-n4">
<div v-for="(account, index) in recommended" class="media align-items-center mb-3">
<img :src="account.avatar" class="avatar shadow-sm mr-3" width="40" height="40">
<div class="media-body">
<p class="lead font-weight-bold username primary">&commat;{{ account.username }}</p>
<p class="text-muted mb-0 display-name">{{ account.display_name }}</p>
</div>
<button class="btn btn-primary btn-sm follow">
{{ $t('profile.follow') }}
</button>
</div>
</div>
</div>
</div>
<div class="d-none card shadow-sm mb-3" style="border-radius: 15px;">
<div class="card-body">
<div class="d-flex justify-content-between">
<p class="text-muted">Trending</p>
<p class="text-lighter"><i class="far fa-cog"></i></p>
</div>
<div class="media-list row mb-n3">
<div v-for="(post, index) in trending" class="col-6 mb-1 p-1">
<img :src="post.url" width="100%" height="100" class="bg-white p-1 shadow-sm" style="object-fit: cover;border-radius: 15px;">
</div>
<div class="col-6 mb-1 p-1 d-flex justify-content-center align-items-center">
<button class="btn btn-link text-dark">
<i class="fal fa-plus-circle fa-lg"></i>
</button>
</div>
</div>
</div>
</div> -->
</div>
</template>
<script type="text/javascript">
import Notifications from './../sections/Notifications.vue';
export default {
components: {
"notifications": Notifications
},
data() {
return {
profile: {},
}
},
mounted() {
this.profile = window._sharedData.user;
}
}
</script>
<style lang="scss" scoped>
.avatar {
border-radius: 15px;
}
.username {
font-size: 15px;
margin-bottom: -6px;
}
.display-name {
font-size: 12px;
}
.follow {
background-color: var(--primary);
border-radius: 18px;
font-weight: 600;
padding: 5px 15px;
}
.btn-white {
background-color: #fff;
border: 1px solid #F3F4F6;
}
</style>

View file

@ -0,0 +1,726 @@
<template>
<div class="sidebar-component sticky-top d-none d-md-block">
<!-- <input type="file" class="d-none" ref="avatarUpdateRef" @change="handleAvatarUpdate()"> -->
<!-- <div class="card shadow-sm mb-3 cursor-pointer" style="border-radius: 15px;" @click="gotoMyProfile()"> -->
<div class="card shadow-sm mb-3" style="border-radius: 15px;">
<div class="card-body p-2">
<div class="media user-card user-select-none">
<div style="position: relative;">
<img :src="user.avatar" class="avatar shadow cursor-pointer" draggable="false" onerror="this.onerror=null;this.src='/storage/avatars/default.png?v=0';" @click="gotoMyProfile()">
<button class="btn btn-light btn-sm avatar-update-btn" @click="updateAvatar()">
<span class="avatar-update-btn-icon"></span>
</button>
</div>
<div class="media-body">
<p class="display-name" v-html="getDisplayName()"></p>
<p class="username primary">&commat;{{ user.username }}</p>
<p class="stats">
<span class="stats-following">
<span class="following-count">{{ formatCount(user.following_count) }}</span> Following
</span>
<span class="stats-followers">
<span class="followers-count">{{ formatCount(user.followers_count) }}</span> Followers
</span>
</p>
</div>
</div>
</div>
</div>
<div class="btn-group btn-group-lg btn-block mb-4">
<!-- <button type="button" class="btn btn-outline-primary btn-block font-weight-bold" style="border-top-left-radius: 18px;border-bottom-left-radius:18px;font-size:18px;font-weight:300!important" @click="createNewPost()">
<i class="fal fa-arrow-circle-up mr-1"></i> {{ $t('navmenu.compose') }} Post
</button> -->
<router-link to="/i/web/compose" class="btn btn-primary btn-block font-weight-bold">
<i class="fal fa-arrow-circle-up mr-1"></i> {{ $t('navmenu.compose') }} Post
</router-link>
<button type="button" class="btn btn-outline-primary dropdown-toggle dropdown-toggle-split" data-toggle="dropdown" aria-expanded="false">
<span class="sr-only">Toggle Dropdown</span>
</button>
<div class="dropdown-menu dropdown-menu-right">
<a class="dropdown-item font-weight-bold" href="/i/collections/create">Create Collection</a>
<a v-if="hasStories" class="dropdown-item font-weight-bold" href="/i/stories/new">Create Story</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item font-weight-bold" href="/settings/home">Account Settings</a>
</div>
</div>
<!-- <router-link to="/i/web/compose" class="btn btn-primary btn-lg btn-block mb-4 shadow-sm font-weight-bold">
<i class="far fa-plus-square mr-1"></i> {{ $t('navmenu.compose') }}
</router-link> -->
<div class="sidebar-sticky shadow-sm">
<ul class="nav flex-column">
<li class="nav-item">
<div class="d-flex justify-content-between align-items-center">
<!-- <router-link class="nav-link text-center" to="/i/web">
<div class="icon text-lighter"><i class="far fa-home fa-lg"></i></div>
<div class="small">{{ $t('navmenu.homeFeed') }}</div>
</router-link> -->
<a
class="nav-link text-center"
href="/i/web"
:class="[ $route.path == '/i/web' ? 'router-link-exact-active active' : '' ]"
@click.prevent="goToFeed('home')">
<div class="icon text-lighter"><i class="far fa-home fa-lg"></i></div>
<div class="small">{{ $t('navmenu.homeFeed') }}</div>
</a>
<!-- <router-link v-if="hasLocalTimeline" class="nav-link text-center" :to="{ name: 'timeline', params: { scope: 'local' } }">
<div class="icon text-lighter"><i class="fas fa-stream fa-lg"></i></div>
<div class="small">{{ $t('navmenu.localFeed') }}</div>
</router-link> -->
<a
v-if="hasLocalTimeline"
class="nav-link text-center"
href="/i/web/timeline/local"
:class="[ $route.path == '/i/web/timeline/local' ? 'router-link-exact-active active' : '' ]"
@click.prevent="goToFeed('local')">
<div class="icon text-lighter"><i class="fas fa-stream fa-lg"></i></div>
<div class="small">{{ $t('navmenu.localFeed') }}</div>
</a>
<!-- <router-link v-if="hasNetworkTimeline" class="nav-link text-center" :to="{ name: 'timeline', params: { scope: 'global' } }">
<div class="icon text-lighter"><i class="far fa-globe fa-lg"></i></div>
<div class="small">{{ $t('navmenu.globalFeed') }}</div>
</router-link> -->
<a
v-if="hasNetworkTimeline"
class="nav-link text-center"
href="/i/web/timeline/global"
:class="[ $route.path == '/i/web/timeline/global' ? 'router-link-exact-active active' : '' ]"
@click.prevent="goToFeed('global')">
<div class="icon text-lighter"><i class="far fa-globe fa-lg"></i></div>
<div class="small">{{ $t('navmenu.globalFeed') }}</div>
</a>
</div>
<hr class="mb-0" style="margin-top: -5px;opacity: 0.4;" />
</li>
<!-- <li class="nav-item">
</li>
<li class="nav-item">
</li> -->
<!-- <li v-for="(link, index) in links" class="nav-item">
<router-link class="nav-link" :to="link.path">
<span v-if="link.icon" class="icon text-lighter"><i :class="[ link.icon ]"></i></span>
{{ link.name }}
</router-link>
</li> -->
<li class="nav-item">
<router-link class="nav-link" to="/i/web/discover">
<span class="icon text-lighter"><i class="far fa-compass"></i></span>
{{ $t('navmenu.discover') }}
</router-link>
</li>
<li class="nav-item">
<router-link class="nav-link d-flex justify-content-between align-items-center" to="/i/web/direct">
<span>
<span class="icon text-lighter">
<i class="far fa-envelope"></i>
</span>
{{ $t('navmenu.directMessages') }}
</span>
<!-- <span class="badge badge-danger font-weight-light rounded-pill px-2" style="transform:scale(0.86)">99+</span> -->
</router-link>
</li>
<!-- <li class="nav-item">
<router-link class="nav-link" to="/i/web/groups">
<span class="icon text-lighter"><i class="far fa-layer-group"></i></span>
{{ $t('navmenu.groups') }}
</router-link>
</li> -->
<li v-if="hasLiveStreams" class="nav-item">
<router-link class="nav-link d-flex justify-content-between align-items-center" to="/i/web/livestreams">
<span>
<span class="icon text-lighter">
<i class="far fa-record-vinyl"></i>
</span>
Livestreams
</span>
</router-link>
</li>
<li class="nav-item">
<router-link class="nav-link d-flex justify-content-between align-items-center" to="/i/web/notifications">
<span>
<span class="icon text-lighter">
<i class="far fa-bell"></i>
</span>
{{ $t('navmenu.notifications') }}
</span>
<!-- <span class="badge badge-danger font-weight-light rounded-pill px-2" style="transform:scale(0.86)">99+</span> -->
</router-link>
</li>
<li class="nav-item">
<hr class="mt-n1" style="opacity: 0.4;margin-bottom: 0;" />
<router-link class="nav-link" :to="'/i/web/profile/' + user.id">
<span class="icon text-lighter">
<i class="far fa-user"></i>
</span>
{{ $t('navmenu.profile') }}
</router-link>
<!-- <router-link class="nav-link" to="/i/web/settings">
<span class="icon text-lighter">
<i class="far fa-cog"></i>
</span>
{{ $t('navmenu.settings') }}
</router-link> -->
</li>
<!-- <li class="nav-item">
<router-link class="nav-link" to="/i/web/drive">
<span class="icon text-lighter">
<i class="far fa-cloud-upload"></i>
</span>
{{ $t('navmenu.drive') }}
</router-link>
</li> -->
<!-- <li class="nav-item">
<router-link class="nav-link" to="/i/web/settings">
<span class="icon text-lighter">
<i class="fas fa-cog"></i>
</span>
{{ $t('navmenu.settings') }}
</router-link>
</li>
<li class="nav-item">
<a class="nav-link" href="/i/web/help">
<span class="icon text-lighter">
<i class="fas fa-info-circle"></i>
</span>
Help
</a>
</li> -->
<li v-if="user.is_admin" class="nav-item">
<hr class="mt-n1" style="opacity: 0.4;margin-bottom: 0;" />
<a class="nav-link" href="/i/admin/dashboard">
<span class="icon text-lighter">
<i class="far fa-tools"></i>
</span>
{{ $t('navmenu.admin') }}
</a>
</li>
<li class="nav-item">
<hr class="mt-n1" style="opacity: 0.4;margin-bottom: 0;" />
<a class="nav-link" href="/?force_old_ui=1">
<span class="icon text-lighter">
<i class="fas fa-chevron-left"></i>
</span>
{{ $t('navmenu.backToPreviousDesign') }}
</a>
</li>
<!-- <li class="nav-item">
<router-link class="nav-link" to="/i/web/?a=feed">
<span class="fas fa-stream pr-2 text-lighter"></span>
Feed
</router-link>
</li>
<li class="nav-item">
<router-link class="nav-link" to="/i/web/discover">
<span class="fas fa-compass pr-2 text-lighter"></span>
Discover
</router-link>
</li>
<li class="nav-item">
<router-link class="nav-link" to="/i/web/stories">
<span class="fas fa-history pr-2 text-lighter"></span>
Stories
</router-link>
</li> -->
</ul>
</div>
<!-- <div class="sidebar-sitelinks">
<a href="/site/about">{{ $t('navmenu.about') }}</a>
<a href="/site/language">{{ $t('navmenu.language') }}</a>
<a href="/site/terms">{{ $t('navmenu.privacy') }}</a>
<a href="/site/terms">{{ $t('navmenu.terms') }}</a>
</div> -->
<div class="sidebar-attribution pr-3 d-flex justify-content-between align-items-center">
<router-link to="/i/web/language">
<i class="fal fa-language fa-2x" alt="Select a language"></i>
</router-link>
<a href="/site/help" class="font-weight-bold">{{ $t('navmenu.help') }}</a>
<a href="/site/privacy" class="font-weight-bold">{{ $t('navmenu.privacy') }}</a>
<a href="/site/terms" class="font-weight-bold">{{ $t('navmenu.terms') }}</a>
<a href="https://pixelfed.org" class="font-weight-bold powered-by">Powered by Pixelfed</a>
</div>
<!-- <b-modal
ref="avatarUpdateModal"
centered
hide-footer
header-class="py-2"
body-class="p-0"
title-class="w-100 text-center pl-4 font-weight-bold"
title-tag="p"
title="Upload Avatar"
>
<div class="d-flex align-items-center justify-content-center">
<div
v-if="avatarUpdateIndex === 0"
class="py-5 user-select-none cursor-pointer"
@click="avatarUpdateStep(0)">
<p class="text-center primary">
<i class="fal fa-cloud-upload fa-3x"></i>
</p>
<p class="text-center lead">Drag photo here or click here</p>
<p class="text-center small text-muted mb-0">Must be a <strong>png</strong> or <strong>jpg</strong> image up to 2MB</p>
</div>
<div v-else-if="avatarUpdateIndex === 1" class="w-100 p-5">
<div class="d-md-flex justify-content-between align-items-center">
<div class="text-center mb-4">
<p class="small font-weight-bold" style="opacity:0.7;">Current</p>
<img :src="user.avatar" class="shadow" style="width: 150px;height: 150px;object-fit: cover;border-radius: 18px;opacity: 0.7;">
</div>
<div class="text-center mb-4">
<p class="font-weight-bold">New</p>
<img :src="avatarUpdateFile" class="shadow" style="width: 220px;height: 220px;object-fit: cover;border-radius: 18px;">
</div>
</div>
<hr>
<div class="d-flex justify-content-between">
<button class="btn btn-light font-weight-bold btn-block mr-3" @click="avatarUpdateClose()">Cancel</button>
<button class="btn btn-primary primary font-weight-bold btn-block mt-0">Upload</button>
</div>
</div>
</div>
</b-modal> -->
<!-- <b-modal
ref="createPostModal"
centered
hide-footer
header-class="py-2"
body-class="p-0 w-100 h-100"
title-class="w-100 text-center pl-4 font-weight-bold"
title-tag="p"
title="Create New Post"
>
<compose-simple />
</b-modal> -->
<update-avatar ref="avatarUpdate" :user="user" />
</div>
</template>
<script type="text/javascript">
import { mapGetters } from 'vuex'
import ComposeSimple from './../sections/ComposeSimple.vue';
import UpdateAvatar from './modal/UpdateAvatar.vue';
export default {
props: {
user: {
type: Object,
default: (function() {
return {
avatar: '/storage/avatars/default.jpg',
username: false,
display_name: '',
following_count: 0,
followers_count: 0
};
})
},
links: {
type: Array,
default: function() {
return [
// {
// name: "Home",
// path: "/i/web",
// icon: "fas fa-home"
// },
// {
// name: "Local",
// path: "/i/web/timeline/local",
// icon: "fas fa-stream"
// },
// {
// name: "Global",
// path: "/i/web/timeline/global",
// icon: "far fa-globe"
// },
// {
// name: "Audiences",
// path: "/i/web/discover",
// icon: "far fa-circle-notch"
// },
{
name: "Discover",
path: "/i/web/discover",
icon: "fas fa-compass"
},
// {
// name: "Events",
// path: "/i/events",
// icon: "far fa-calendar-alt"
// },
{
name: "Groups",
path: "/i/web/groups",
icon: "far fa-user-friends"
},
// {
// name: "Live",
// path: "/i/web/?t=live",
// icon: "far fa-play"
// },
// {
// name: "Marketplace",
// path: "/i/web/marketplace",
// icon: "far fa-shopping-cart"
// },
// {
// name: "Stories",
// path: "/i/web/?t=stories",
// icon: "fas fa-history"
// },
{
name: "Videos",
path: "/i/web/videos",
icon: "far fa-video"
}
];
}
}
},
components: {
ComposeSimple,
UpdateAvatar
},
computed: {
...mapGetters([
'getCustomEmoji'
])
},
data() {
return {
loaded: false,
hasLocalTimeline: true,
hasNetworkTimeline: false,
hasLiveStreams: false,
hasStories: false,
}
},
mounted() {
if(window.App.config.features.hasOwnProperty('timelines')) {
this.hasLocalTimeline = App.config.features.timelines.local;
this.hasNetworkTimeline = App.config.features.timelines.network;
//this.hasLiveStreams = App.config.ab.hls == true;
}
if(window.App.config.features.hasOwnProperty('stories')) {
this.hasStories = App.config.features.stories;
}
// if(!this.user.username) {
// this.user = window._sharedData.user;
// }
// setTimeout(() => {
// this.user = window._sharedData.curUser;
// this.loaded = true;
// }, 300);
},
methods: {
getDisplayName() {
let self = this;
let profile = this.user;
let dn = profile.display_name;
if(!dn) {
return profile.username;
}
if(dn.includes(':')) {
let re = /(<a?)?:\w+:(\d{18}>)?/g;
let un = dn.replaceAll(re, function(em) {
let shortcode = em.slice(1, em.length - 1);
let emoji = self.getCustomEmoji.filter(e => {
return e.shortcode == shortcode;
});
return emoji.length ? `<img draggable="false" class="emojione custom-emoji" alt="${emoji[0].shortcode}" title="${emoji[0].shortcode}" src="${emoji[0].url}" data-original="${emoji[0].url}" data-static="${emoji[0].static_url}" width="16" height="16" onerror="this.onerror=null;this.src='/storage/emoji/missing.png';" />`: em;
});
return un;
} else {
return dn;
}
},
gotoMyProfile() {
let user = this.user;
this.$router.push({
name: 'profile',
path: `/i/web/profile/${user.id}`,
params: {
id: user.id,
cachedProfile: user,
cachedUser: user
}
})
},
formatCount(count = 0, locale = 'en-GB', notation = 'compact') {
return new Intl.NumberFormat(locale, { notation: notation , compactDisplay: "short" }).format(count);
},
updateAvatar() {
event.currentTarget.blur();
// swal('update avatar', 'test', 'success');
this.$refs.avatarUpdate.open();
},
createNewPost() {
this.$refs.createPostModal.show();
},
goToFeed(feed) {
const curPath = this.$route.path;
switch(feed) {
case 'home':
if(curPath == '/i/web') {
this.$emit('refresh');
} else {
this.$router.push('/i/web');
}
break;
case 'local':
if(curPath == '/i/web/timeline/local') {
this.$emit('refresh');
} else {
this.$router.push({ name: 'timeline', params: { scope: 'local' }});
}
break;
case 'global':
if(curPath == '/i/web/timeline/global') {
this.$emit('refresh');
} else {
this.$router.push({ name: 'timeline', params: { scope: 'global' }});
}
break;
}
}
}
}
</script>
<style lang="scss">
.sidebar-component {
.sidebar-sticky {
background-color: var(--card-bg);
border-radius: 15px;
}
&.sticky-top {
top: 90px;
}
.nav {
overflow: auto;
}
.nav-item {
.nav-link {
font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
font-weight: 500;
color: rgba(156,163,175, 1);
padding-left: 14px;
margin-bottom: 5px;
&:hover {
background-color: var(--light-hover-bg);
}
.icon {
display: inline-block;
width: 40px;
text-align: center;
}
}
.router-link-exact-active {
color: var(--primary);
font-weight: 700;
padding-left: 14px;
&:not(.text-center) {
padding-left: 10px;
border-left: 4px solid var(--primary);
}
.icon {
color: var(--primary) !important;
}
}
&:first-child {
.nav-link {
.small {
font-weight: 700;
}
&:first-child {
border-top-left-radius: 15px;
}
&:last-child {
border-top-right-radius: 15px;
}
}
}
&:is(:last-child) {
.nav-link {
margin-bottom: 0;
border-bottom-left-radius: 15px;
border-bottom-right-radius: 15px;
}
}
}
.sidebar-heading {
font-size: .75rem;
text-transform: uppercase;
}
.user-card {
align-items: center;
.avatar {
width: 75px;
height: 75px;
border-radius: 15px;
margin-right: 0.8rem;
border: 1px solid var(--border-color);
}
.avatar-update-btn {
position: absolute;
right: 12px;
bottom: 0;
width: 20px;
height: 20px;
background: rgba(255,255,255,0.9);
border: 1px solid #dee2e6 !important;
padding: 0;
border-radius: 50rem;
&-icon {
font-family: 'Font Awesome 5 Free';
font-weight: 400;
-webkit-font-smoothing: antialiased;
display: inline-block;
font-style: normal;
font-variant: normal;
text-rendering: auto;
line-height: 1;
&:before {
content: "\F013";
}
}
}
.username {
font-weight: 600;
font-size: 13px;
margin-bottom: 0;
}
.display-name {
color: var(--body-color);
line-height: 0.8;
font-size: 14px;
font-weight: 800 !important;
user-select: all;
font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
margin-bottom: 0;
word-break: break-all;
}
.stats {
margin-top: 0;
margin-bottom: 0;
font-size: 12px;
user-select: none;
.stats-following {
margin-right: 0.8rem;
}
.following-count,
.followers-count {
font-weight: 800;
}
}
}
.btn-primary {
background-color: var(--primary);
&.router-link-exact-active {
opacity: 0.5;
pointer-events: none;
cursor: unset;
}
}
.sidebar-sitelinks {
margin-top: 1rem;
display: flex;
justify-content: space-between;
padding: 0 2rem;
a {
font-size: 12px;
color: #B8C2CC;
}
.active {
color: #212529;
font-weight: 600;
}
}
.sidebar-attribution {
margin-top: 0.5rem;
font-size: 10px;
color: #B8C2CC;
padding-left: 2rem;
a {
color: #B8C2CC !important;
&.powered-by {
opacity: 0.5;
}
}
}
}
</style>

View file

@ -0,0 +1,234 @@
<template>
<div class="media mb-2 align-items-center px-3 shadow-sm py-2 bg-white" style="border-radius: 15px;">
<a href="#" @click.prevent="getProfileUrl(n.account)" v-b-tooltip.hover :title="n.account.acct">
<img class="mr-3 shadow-sm" style="border-radius:8px" :src="n.account.avatar" alt="" width="40" height="40" onerror="this.onerror=null;this.src='/storage/avatars/default.jpg';">
</a>
<div class="media-body font-weight-light">
<div v-if="n.type == 'favourite'">
<p class="my-0">
<a :href="displayProfileUrl(n.account)" @click.prevent="getProfileUrl(n.account)" class="font-weight-bold text-dark text-break" :title="n.account.acct">&commat;{{n.account.acct}}</a> {{ $t('notifications.liked') }} <a class="font-weight-bold" :href="displayPostUrl(n.status)" @click.prevent="getPostUrl(n.status)">post</a>.
</p>
</div>
<div v-else-if="n.type == 'comment'">
<p class="my-0">
<a :href="displayProfileUrl(n.account)" @click.prevent="getProfileUrl(n.account)" class="font-weight-bold text-dark text-break" :title="n.account.acct">&commat;{{n.account.acct}}</a> {{ $t('notifications.commented') }} <a class="font-weight-bold" :href="displayPostUrl(n.status)" @click.prevent="getPostUrl(n.status)">post</a>.
</p>
</div>
<!-- <div v-else-if="n.type == 'group:comment'">
<p class="my-0">
<a href="#" @click.prevent="getProfileUrl(n.account)" class="font-weight-bold text-dark text-break" :title="n.account.acct">&commat;{{n.account.acct}}</a> {{ $t('notifications.commented') }} <a class="font-weight-bold" v-bind:href="n.group_post_url">{{ $('notifications.groupPost') }}</a>.
</p>
</div> -->
<div v-else-if="n.type == 'story:react'">
<p class="my-0">
<a :href="displayProfileUrl(n.account)" @click.prevent="getProfileUrl(n.account)" class="font-weight-bold text-dark text-break" :title="n.account.acct">&commat;{{n.account.acct}}</a> {{ $t('notifications.reacted') }} <a class="font-weight-bold" v-bind:href="'/account/direct/t/'+n.account.id">story</a>.
</p>
</div>
<div v-else-if="n.type == 'story:comment'">
<p class="my-0">
<a :href="displayProfileUrl(n.account)" @click.prevent="getProfileUrl(n.account)" class="font-weight-bold text-dark text-break" :title="n.account.acct">&commat;{{n.account.acct}}</a> {{ $t('notifications.commented') }} <a class="font-weight-bold" v-bind:href="'/account/direct/t/'+n.account.id">{{ $t('notifications.story') }}</a>.
</p>
</div>
<div v-else-if="n.type == 'mention'">
<p class="my-0">
<a :href="displayProfileUrl(n.account)" @click.prevent="getProfileUrl(n.account)" class="font-weight-bold text-dark text-break" :title="n.account.acct">&commat;{{n.account.acct}}</a> <a class="font-weight-bold" v-bind:href="mentionUrl(n.status)">{{ $t('notifications.mentioned') }}</a> {{ $t('notifications.you') }}.
</p>
</div>
<div v-else-if="n.type == 'follow'">
<p class="my-0">
<a :href="displayProfileUrl(n.account)" @click.prevent="getProfileUrl(n.account)" class="font-weight-bold text-dark text-break" :title="n.account.acct">&commat;{{n.account.acct}}</a> {{ $t('notifications.followed') }} {{ $t('notifications.you') }}.
</p>
</div>
<div v-else-if="n.type == 'share'">
<p class="my-0">
<a :href="displayProfileUrl(n.account)" @click.prevent="getProfileUrl(n.account)" class="font-weight-bold text-dark text-break" :title="n.account.acct">&commat;{{n.account.acct}}</a> {{ $t('notifications.shared') }} <a class="font-weight-bold" :href="displayPostUrl(n.status)" @click.prevent="getPostUrl(n.status)">{{ $t('notifications.post') }}</a>.
</p>
</div>
<div v-else-if="n.type == 'modlog'">
<p class="my-0">
<a :href="displayProfileUrl(n.account)" @click.prevent="getProfileUrl(n.account)" class="font-weight-bold text-dark text-break" :title="n.account.acct">{{truncate(n.account.username)}}</a> {{ $t('notifications.updatedA') }} <a class="font-weight-bold" v-bind:href="n.modlog.url">{{ $t('notifications.modlog') }}</a>.
</p>
</div>
<div v-else-if="n.type == 'tagged'">
<p class="my-0">
<a :href="displayProfileUrl(n.account)" @click.prevent="getProfileUrl(n.account)" class="font-weight-bold text-dark text-break" :title="n.account.acct">&commat;{{n.account.acct}}</a> {{ $t('notifications.tagged') }} <a class="font-weight-bold" v-bind:href="n.tagged.post_url">{{ $t('notifications.post') }}</a>.
</p>
</div>
<div v-else-if="n.type == 'direct'">
<p class="my-0">
<a :href="displayProfileUrl(n.account)" @click.prevent="getProfileUrl(n.account)" class="font-weight-bold text-dark text-break" :title="n.account.acct">&commat;{{n.account.acct}}</a> {{ $t('notifications.sentA') }} <router-link class="font-weight-bold" :to="'/i/web/direct/thread/'+n.account.id">{{ $t('notifications.dm') }}</router-link>.
</p>
</div>
<div v-else-if="n.type == 'group.join.approved'">
<p class="my-0">
{{ $t('notifications.yourApplication') }} <a :href="n.group.url" class="font-weight-bold text-dark text-break" :title="n.group.name">{{truncate(n.group.name)}}</a> {{ $t('notifications.applicationApproved') }}
</p>
</div>
<div v-else-if="n.type == 'group.join.rejected'">
<p class="my-0">
{{ $t('notifications.yourApplication') }} <a :href="n.group.url" class="font-weight-bold text-dark text-break" :title="n.group.name">{{truncate(n.group.name)}}</a> {{ $t('notifications.applicationRejected') }}
</p>
</div>
<div v-else>
<p class="my-0 d-flex justify-content-between align-items-center">
<span class="font-weight-bold">Notification</span>
<span style="font-size:8px;">e_{{ n.type }}::{{ n.id }}</span>
</p>
</div>
<div class="align-items-center">
<span class="small text-muted" data-toggle="tooltip" data-placement="bottom" :title="n.created_at">{{timeAgo(n.created_at)}}</span>
</div>
</div>
<div>
<div v-if="n.status && n.status && n.status.media_attachments && n.status.media_attachments.length">
<a href="#" @click.prevent="getPostUrl(n.status)">
<img :src="n.status.media_attachments[0].preview_url" width="32px" height="32px">
</a>
</div>
<div v-else-if="n.status && n.status.parent && n.status.parent.media_attachments && n.status.parent.media_attachments.length">
<a :href="n.status.parent.url">
<img :src="n.status.parent.media_attachments[0].preview_url" width="32px" height="32px">
</a>
</div>
<!-- <div v-else-if="n.status && n.status.parent && n.status.parent.media_attachments && n.status.parent.media_attachments.length">
<a :href="n.status.parent.url">
<img :src="n.status.parent.media_attachments[0].preview_url" width="32px" height="32px">
</a>
</div> -->
<!-- <div v-else>
<a v-if="viewContext(n) != '/'" class="btn btn-outline-primary py-0 font-weight-bold" href="#" @click.prevent="viewContext(n)">View</a>
</div> -->
</div>
</div>
</template>
<script type="text/javascript">
export default {
props: {
n: {
type: Object
}
},
data() {
return {
profile: window._sharedData.user
};
},
methods: {
truncate(text, limit = 30) {
if(text.length <= limit) {
return text;
}
return text.slice(0, limit) + '...'
},
timeAgo(ts) {
let date = Date.parse(ts);
let seconds = Math.floor((new Date() - date) / 1000);
let interval = Math.floor(seconds / 31536000);
if (interval >= 1) {
return interval + "y";
}
interval = Math.floor(seconds / 604800);
if (interval >= 1) {
return interval + "w";
}
interval = Math.floor(seconds / 86400);
if (interval >= 1) {
return interval + "d";
}
interval = Math.floor(seconds / 3600);
if (interval >= 1) {
return interval + "h";
}
interval = Math.floor(seconds / 60);
if (interval >= 1) {
return interval + "m";
}
return Math.floor(seconds) + "s";
},
mentionUrl(status) {
let username = status.account.username;
let id = status.id;
return '/p/' + username + '/' + id;
},
viewContext(n) {
switch(n.type) {
case 'follow':
return this.getProfileUrl(n.account);
return n.account.url;
break;
case 'mention':
return n.status.url;
break;
case 'like':
case 'favourite':
case 'comment':
return this.getPostUrl(n.status);
// return n.status.url;
break;
case 'tagged':
return n.tagged.post_url;
break;
case 'direct':
return '/account/direct/t/'+n.account.id;
break
}
return '/';
},
displayProfileUrl(account) {
return `/i/web/profile/${account.id}`;
},
displayPostUrl(status) {
return `/i/web/post/${status.id}`;
},
getProfileUrl(account) {
this.$router.push({
name: 'profile',
path: `/i/web/profile/${account.id}`,
params: {
id: account.id,
cachedProfile: account,
cachedUser: this.profile
}
});
},
getPostUrl(status) {
this.$router.push({
name: 'post',
path: `/i/web/post/${status.id}`,
params: {
id: status.id,
cachedStatus: status,
cachedProfile: this.profile
}
});
}
}
}
</script>

View file

@ -0,0 +1,200 @@
<template>
<div class="story-carousel-component">
<div v-if="canShow" class="d-flex story-carousel-component-wrapper" style="overflow-y: auto;z-index: 3;">
<a class="col-4 col-lg-3 col-xl-2 px-1 text-dark text-decoration-none" href="/i/stories/new" style="max-width: 120px;">
<template v-if="selfStory && selfStory.length">
<div
class="story-wrapper text-white shadow-sm mb-3"
:style="{ background: `linear-gradient(rgba(0,0,0,0.2),rgba(0,0,0,0.4)), url(${selfStory[0].latest.preview_url})`, backgroundSize: 'cover', backgroundPosition: 'center'}"
style="width: 100%;height:200px;border-radius:15px;">
<div class="story-wrapper-blur d-flex flex-column align-items-center justify-content-between" style="display: block;width: 100%;height:100%;">
<p class="mb-4"></p>
<p class="mb-0"><i class="fal fa-plus-circle fa-2x"></i></p>
<p class="font-weight-bold">My Story</p>
</div>
</div>
</template>
<template v-else>
<div
class="story-wrapper text-white shadow-sm d-flex flex-column align-items-center justify-content-between"
style="width: 100%;height:200px;border-radius:15px;">
<p class="mb-4"></p>
<p class="mb-0"><i class="fal fa-plus-circle fa-2x"></i></p>
<p class="font-weight-bold">{{ $t('story.add') }}</p>
</div>
</template>
</a>
<div v-for="(story, index) in stories" class="col-4 col-lg-3 col-xl-2 px-1" style="max-width: 120px;">
<template v-if="story.hasOwnProperty('url')">
<a class="story" :href="story.url">
<div
v-if="story.latest && story.latest.type == 'photo'"
class="shadow-sm story-wrapper"
:class="{ seen: story.seen }"
:style="{ background: `linear-gradient(rgba(0,0,0,0.2),rgba(0,0,0,0.4)), url(${story.latest.preview_url})`, backgroundSize: 'cover', backgroundPosition: 'center'}">
<div class="story-wrapper-blur" style="display: block;width: 100%;height:100%;position:relative;">
<div class="px-2" style="display: block;width: 100%;bottom:0;position: absolute;">
<p class="mt-3 mb-0">
<img :src="story.avatar" width="30" height="30" class="avatar" draggable="false" onerror="this.onerror=null;this.src='/storage/avatars/default.jpg?v=0';">
</p>
<p class="mb-0"></p>
<p class="username font-weight-bold small text-truncate">
{{ story.username }}
</p>
</div>
</div>
</div>
<div
v-else
class="shadow-sm story-wrapper">
<div class="px-2" style="display: block;width: 100%;bottom:0;position: absolute;">
<p class="mt-3 mb-0">
<img :src="story.avatar" width="30" height="30" class="avatar">
</p>
<p class="mb-0"></p>
<p class="username font-weight-bold small text-truncate">
{{ story.username }}
</p>
</div>
</div>
</a>
</template>
<template v-else>
<div
class="story shadow-sm story-wrapper seen"
:style="{ background: `linear-gradient(rgba(0,0,0,0.01),rgba(0,0,0,0.04))`}">
<div class="story-wrapper-blur" style="display: block;width: 100%;height:100%;position:relative;">
<div class="px-2" style="display: block;width: 100%;bottom:0;position: absolute;">
<p class="mt-3 mb-0">
</p>
<p class="mb-0"></p>
<p class="username font-weight-bold small text-truncate">
</p>
</div>
</div>
</div>
</template>
</div>
<template v-if="selfStory && selfStory.length && stories.length < 2">
<div v-for="i in 5" class="col-4 col-lg-3 col-xl-2 px-1 story" style="max-width: 120px;">
<div
class="shadow-sm story-wrapper seen"
:style="{ background: `linear-gradient(rgba(0,0,0,0.01),rgba(0,0,0,0.04))`}">
<div class="story-wrapper-blur" style="display: block;width: 100%;height:100%;position:relative;">
<div class="px-2" style="display: block;width: 100%;bottom:0;position: absolute;">
<p class="mt-3 mb-0">
</p>
<p class="mb-0"></p>
<p class="username font-weight-bold small text-truncate">
</p>
</div>
</div>
</div>
</div>
</template>
</div>
</div>
</template>
<script type="text/javascript">
export default {
props: {
profile: {
type: Object
}
},
data() {
return {
canShow: false,
stories: [],
selfStory: undefined
}
},
mounted() {
this.fetchStories();
},
methods: {
fetchStories() {
axios.get('/api/web/stories/v1/recent')
.then(res => {
if(res.data && res.data.length) {
this.selfStory = res.data.filter(s => s.pid == this.profile.id);
let activeStories = res.data.filter(s => s.pid !== this.profile.id);
this.stories = activeStories;
this.canShow = true;
if(!activeStories || !activeStories.length || activeStories.length < 5) {
this.stories.push(...Array(5 - activeStories.length).keys())
}
}
})
}
}
}
</script>
<style lang="scss">
.story-carousel-component {
&-wrapper {
-ms-overflow-style: none;
scrollbar-width: none;
&::-webkit-scrollbar {
width: 0 !important
}
}
.story {
&-wrapper {
display: block;
position: relative;
width: 100%;
height: 200px;
border-radius: 15px;
margin-bottom: 1rem;
background: #b24592;
background: -webkit-linear-gradient(to right, #b24592, #f15f79);
background: linear-gradient(to right, #b24592, #f15f79);
overflow: hidden;
border: 1px solid var(--border-color);
.username {
color: #fff;
}
.avatar {
border-radius: 6px;
margin-bottom: 5px;
}
&.seen {
opacity: 30%;
}
&-blur {
border-radius: 15px;
overflow: hidden;
background: rgba(0, 0, 0, 0.2);
backdrop-filter: blur(8px);
}
}
}
}
.force-dark-mode {
.story-wrapper {
&.seen {
opacity: 50%;
background: linear-gradient(rgba(255,255,255,0.12),rgba(255,255,255,0.14)) !important;
}
}
}
</style>