浏览代码

Add partial components

Daniel Supernault 2 年之前
父节点
当前提交
f5dbc8281a

+ 19 - 0
resources/assets/components/partials/StatusPlaceholder.vue

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

+ 468 - 0
resources/assets/components/partials/TimelineStatus.vue

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

+ 100 - 0
resources/assets/components/partials/rightbar.vue

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

+ 726 - 0
resources/assets/components/partials/sidebar.vue

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

+ 234 - 0
resources/assets/components/partials/timeline/Notification.vue

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

+ 200 - 0
resources/assets/components/partials/timeline/StoryCarousel.vue

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