mirror of
https://github.com/pixelfed/pixelfed.git
synced 2024-09-07 18:08:03 +00:00
Add Groups vues
This commit is contained in:
parent
3d6b9badf4
commit
3811a1cd65
65 changed files with 15176 additions and 0 deletions
51
resources/assets/components/GroupCreate.vue
Normal file
51
resources/assets/components/GroupCreate.vue
Normal file
|
@ -0,0 +1,51 @@
|
|||
<template>
|
||||
<div class="group-notifications-component">
|
||||
<div class="row border-bottom m-0 p-0">
|
||||
<sidebar />
|
||||
|
||||
<create-group />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script type="text/javascript">
|
||||
import SidebarComponent from '@/groups/sections/Sidebar.vue';
|
||||
import LoaderComponent from '@/groups/sections/Loader.vue';
|
||||
import CreateGroup from '@/groups/CreateGroup.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
"sidebar": SidebarComponent,
|
||||
"loader": LoaderComponent,
|
||||
"create-group": CreateGroup
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
loaded: false,
|
||||
loadTimeout: undefined,
|
||||
}
|
||||
},
|
||||
|
||||
created() {
|
||||
this.loadTimeout = setTimeout(() => {
|
||||
this.loaded = true;
|
||||
}, 1000);
|
||||
},
|
||||
|
||||
beforeUnmount() {
|
||||
clearTimeout(this.loadTimeout);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.group-notifications-component {
|
||||
font-family: var(--font-family-sans-serif);
|
||||
|
||||
.jumbotron {
|
||||
background-color: #fff;
|
||||
border-radius: 0px;
|
||||
}
|
||||
}
|
||||
</style>
|
83
resources/assets/components/GroupDiscover.vue
Normal file
83
resources/assets/components/GroupDiscover.vue
Normal file
|
@ -0,0 +1,83 @@
|
|||
<template>
|
||||
<div class="group-discover-component">
|
||||
<div class="row border-bottom m-0 p-0">
|
||||
<sidebar />
|
||||
|
||||
<div class="col-12 col-md-9 px-md-0">
|
||||
<loader v-if="!loaded" :loaded="loaded" />
|
||||
|
||||
<template v-else>
|
||||
<div class="container-fluid">
|
||||
<div class="py-5">
|
||||
<h1>Discover</h1>
|
||||
</div>
|
||||
|
||||
<div class="popular row">
|
||||
<group-card
|
||||
v-for="(popular, idx) in popularGroups"
|
||||
:key="idx"
|
||||
:group="popular"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script type="text/javascript">
|
||||
import SidebarComponent from '@/groups/sections/Sidebar.vue';
|
||||
import LoaderComponent from '@/groups/sections/Loader.vue';
|
||||
import GroupCard from '@/groups/partials/GroupCard.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
"sidebar": SidebarComponent,
|
||||
"loader": LoaderComponent,
|
||||
"group-card": GroupCard
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
loaded: false,
|
||||
loadTimeout: undefined,
|
||||
popularGroups: [],
|
||||
newGroups: [],
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
fetchPopular() {
|
||||
axios.get('/api/v0/groups/discover/popular')
|
||||
.then(res => this.popularGroups = res.data)
|
||||
.finally(() => this.fetchNewGroups())
|
||||
},
|
||||
|
||||
fetchNewGroups() {
|
||||
axios.get('/api/v0/groups/discover/new')
|
||||
.then(res => this.newGroups = res.data)
|
||||
.finally(() => this.loaded = true)
|
||||
},
|
||||
},
|
||||
|
||||
created() {
|
||||
this.fetchPopular()
|
||||
},
|
||||
|
||||
beforeUnmount() {
|
||||
clearTimeout(this.loadTimeout);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.group-discover-component {
|
||||
font-family: var(--font-family-sans-serif);
|
||||
|
||||
.jumbotron {
|
||||
background-color: #fff;
|
||||
border-radius: 0px;
|
||||
}
|
||||
}
|
||||
</style>
|
315
resources/assets/components/GroupFeed.vue
Normal file
315
resources/assets/components/GroupFeed.vue
Normal file
|
@ -0,0 +1,315 @@
|
|||
<template>
|
||||
<div class="groups-home-component w-100 h-100">
|
||||
<div v-if="initialLoad" class="row border-bottom m-0 p-0">
|
||||
<sidebar />
|
||||
|
||||
<self-feed :profile="profile" v-on:switchtab="switchTab" />
|
||||
<!-- <self-discover v-if="tab == 'discover'" :profile="profile" />
|
||||
<self-notifications v-if="tab == 'notifications'" :profile="profile" />
|
||||
<self-invitations v-if="tab == 'invitations'" :profile="profile" />
|
||||
<self-remote-search v-if="tab == 'remotesearch'" :profile="profile" />
|
||||
<self-groups v-if="tab == 'mygroups'" :profile="profile" />
|
||||
<create-group v-if="tab == 'creategroup'" :profile="profile" />
|
||||
<div v-if="tab == 'gsearch'">
|
||||
<div class="col-12 px-5">
|
||||
<div class="my-4">
|
||||
<p class="h1 font-weight-bold mb-1">Group Search</p>
|
||||
<p class="lead text-muted mb-0">Search and explore groups.</p>
|
||||
</div>
|
||||
<div class="media align-items-center text-lighter">
|
||||
<i class="far fa-chevron-left fa-lg mr-3"></i>
|
||||
<div class="media-body">
|
||||
<p class="lead mb-0">Use the search bar on the side menu</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
</div>
|
||||
<div v-else class="row justify-content-center mt-5">
|
||||
<b-spinner />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script type="text/javascript">
|
||||
import GroupStatus from '@/groups/partials/GroupStatus.vue';
|
||||
import SelfFeed from '@/groups/partials/SelfFeed.vue';
|
||||
import SelfDiscover from '@/groups/partials/SelfDiscover.vue';
|
||||
import SelfGroups from '@/groups/partials/SelfGroups.vue';
|
||||
import SelfNotifications from '@/groups/partials/SelfNotifications.vue';
|
||||
import SelfInvitations from '@/groups/partials/SelfInvitations.vue';
|
||||
import SelfRemoteSearch from '@/groups/partials/SelfRemoteSearch.vue';
|
||||
import CreateGroup from '@/groups/CreateGroup.vue';
|
||||
import SidebarComponent from '@/groups/sections/Sidebar.vue';
|
||||
import Autocomplete from '@trevoreyre/autocomplete-vue'
|
||||
import '@trevoreyre/autocomplete-vue/dist/style.css'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
initialLoad: false,
|
||||
config: {},
|
||||
groups: [],
|
||||
profile: {},
|
||||
tab: null,
|
||||
searchQuery: undefined,
|
||||
};
|
||||
},
|
||||
|
||||
components: {
|
||||
'autocomplete-input': Autocomplete,
|
||||
'group-status': GroupStatus,
|
||||
'self-discover': SelfDiscover,
|
||||
'self-groups': SelfGroups,
|
||||
'self-feed': SelfFeed,
|
||||
'self-notifications': SelfNotifications,
|
||||
'self-invitations': SelfInvitations,
|
||||
'self-remote-search': SelfRemoteSearch,
|
||||
"create-group": CreateGroup,
|
||||
"sidebar": SidebarComponent
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.fetchConfig();
|
||||
},
|
||||
|
||||
methods: {
|
||||
init() {
|
||||
document.querySelectorAll("footer").forEach(e => e.parentNode.removeChild(e));
|
||||
document.querySelectorAll(".mobile-footer-spacer").forEach(e => e.parentNode.removeChild(e));
|
||||
document.querySelectorAll(".mobile-footer").forEach(e => e.parentNode.removeChild(e));
|
||||
// let u = new URLSearchParams(window.location.search);
|
||||
// if(u.has('ct')) {
|
||||
// if(['mygroups', 'notifications', 'discover', 'remotesearch', 'creategroup', 'gsearch'].includes(u.get('ct'))) {
|
||||
// if(u.get('ct') == 'creategroup' && this.config.limits.user.create.new == false) {
|
||||
// this.tab = 'feed';
|
||||
// history.pushState(null, null, '/groups/feed');
|
||||
// } else {
|
||||
// this.tab = u.get('ct');
|
||||
// }
|
||||
// } else {
|
||||
// this.tab = 'feed';
|
||||
// history.pushState(null, null, '/groups/feed');
|
||||
// }
|
||||
// } else {
|
||||
// this.tab = 'feed';
|
||||
// }
|
||||
this.initialLoad = true;
|
||||
},
|
||||
|
||||
fetchConfig() {
|
||||
axios.get('/api/v0/groups/config')
|
||||
.then(res => {
|
||||
this.config = res.data;
|
||||
this.fetchProfile();
|
||||
});
|
||||
},
|
||||
|
||||
fetchProfile() {
|
||||
axios.get('/api/pixelfed/v1/accounts/verify_credentials')
|
||||
.then(res => {
|
||||
this.profile = res.data;
|
||||
this.init();
|
||||
window._sharedData.curUser = res.data;
|
||||
window.App.util.navatar();
|
||||
})
|
||||
},
|
||||
|
||||
fetchSelfGroups() {
|
||||
axios.get('/api/v0/groups/self/list')
|
||||
.then(res => {
|
||||
this.groups = res.data;
|
||||
});
|
||||
},
|
||||
|
||||
switchTab(tab) {
|
||||
event.currentTarget.blur();
|
||||
window.scrollTo(0,0);
|
||||
this.tab = tab;
|
||||
|
||||
if(tab != 'feed') {
|
||||
history.pushState(null, null, '/groups/home?ct=' + tab);
|
||||
} else {
|
||||
history.pushState(null, null, '/groups/home');
|
||||
}
|
||||
},
|
||||
|
||||
autocompleteSearch(input) {
|
||||
if (!input || input.length < 2) {
|
||||
if(this.tab = 'searchresults') {
|
||||
this.tab = 'feed';
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
this.searchQuery = input;
|
||||
// this.tab = 'searchresults';
|
||||
|
||||
if(input.startsWith('http')) {
|
||||
let url = new URL(input);
|
||||
if(url.hostname == location.hostname) {
|
||||
location.href = input;
|
||||
return [];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
if(input.startsWith('#')) {
|
||||
this.$bvToast.toast(input, {
|
||||
title: 'Hashtag detected',
|
||||
variant: 'info',
|
||||
autoHideDelay: 5000
|
||||
});
|
||||
return [];
|
||||
}
|
||||
|
||||
return axios.post('/api/v0/groups/search/global', {
|
||||
q: input,
|
||||
v: '0.2'
|
||||
})
|
||||
.then(res => {
|
||||
this.searchLoading = false;
|
||||
return res.data;
|
||||
}).catch(err => {
|
||||
|
||||
if(err.response.status === 422) {
|
||||
this.$bvToast.toast(err.response.data.error.message, {
|
||||
title: 'Cannot display search results',
|
||||
variant: 'danger',
|
||||
autoHideDelay: 5000
|
||||
});
|
||||
}
|
||||
|
||||
return [];
|
||||
})
|
||||
},
|
||||
|
||||
getSearchResultValue(result) {
|
||||
return result.name;
|
||||
},
|
||||
|
||||
onSearchSubmit(result) {
|
||||
if (result.length < 1) {
|
||||
return [];
|
||||
}
|
||||
|
||||
location.href = result.url;
|
||||
},
|
||||
|
||||
truncateName(val) {
|
||||
if(val.length < 24) {
|
||||
return val;
|
||||
}
|
||||
|
||||
return val.substr(0, 23) + '...';
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.groups-home-component {
|
||||
font-family: var(--font-family-sans-serif);
|
||||
|
||||
.group-nav-btn {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding-left: 0;
|
||||
padding-top: 0.3rem;
|
||||
padding-bottom: 0.3rem;
|
||||
margin-bottom: 0.3rem;
|
||||
border-radius: 1.5rem;
|
||||
text-align: left;
|
||||
color: #6c757d;
|
||||
background-color: transparent;
|
||||
border-color: transparent;
|
||||
justify-content: flex-start;
|
||||
|
||||
&.active {
|
||||
background-color: #EFF6FF !important;
|
||||
border:1px solid #DBEAFE !important;
|
||||
color: #212529;
|
||||
|
||||
.group-nav-btn-icon {
|
||||
background-color: #2c78bf !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
}
|
||||
|
||||
&-icon {
|
||||
display: inline-flex;
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
padding: 12px;
|
||||
background-color: #E5E7EB;
|
||||
border-radius: 17px;
|
||||
margin: auto 0.3rem;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
&-name {
|
||||
display: inline-block;
|
||||
margin-left: 0.3rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
.autocomplete-input {
|
||||
height: 2.375rem;
|
||||
background-color: #f8f9fa !important;
|
||||
font-size: 0.9rem;
|
||||
color: #495057;
|
||||
border-radius: 50rem;
|
||||
border-color: transparent;
|
||||
|
||||
&:focus,
|
||||
&[aria-expanded=true] {
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
.autocomplete-result {
|
||||
background: none;
|
||||
padding: 12px;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background-color: #EFF6FF !important;
|
||||
}
|
||||
|
||||
.media {
|
||||
img {
|
||||
object-fit: cover;
|
||||
border-radius: 4px;
|
||||
margin-right: 0.6rem;
|
||||
}
|
||||
|
||||
.icon-placeholder {
|
||||
display: flex;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background-color: #2c78bf;
|
||||
border-radius: 4px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: #fff;
|
||||
margin-right: 0.6rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.autocomplete-result-list {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.fade-enter-active, .fade-leave-active {
|
||||
transition: opacity 200ms;
|
||||
}
|
||||
|
||||
.fade-enter, .fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
79
resources/assets/components/GroupJoins.vue
Normal file
79
resources/assets/components/GroupJoins.vue
Normal file
|
@ -0,0 +1,79 @@
|
|||
<template>
|
||||
<div class="group-joins-component">
|
||||
<div class="row border-bottom m-0 p-0">
|
||||
<sidebar />
|
||||
|
||||
<div class="col-12 col-md-9 px-0 mx-0">
|
||||
<loader v-if="!loaded" :loaded="loaded" />
|
||||
|
||||
<template v-else>
|
||||
<div class="px-5 pt-4 pb-2">
|
||||
<h2 class="fw-bold">My Groups</h2>
|
||||
<self-groups :profile="profile" />
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script type="text/javascript">
|
||||
import SidebarComponent from '@/groups/sections/Sidebar.vue';
|
||||
import LoaderComponent from '@/groups/sections/Loader.vue';
|
||||
import SelfGroups from '@/groups/partials/SelfGroups.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
"sidebar": SidebarComponent,
|
||||
"loader": LoaderComponent,
|
||||
"self-groups": SelfGroups
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
loaded: false,
|
||||
loadTimeout: undefined,
|
||||
config: {},
|
||||
groups: [],
|
||||
profile: {},
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
init() {
|
||||
document.querySelectorAll("footer").forEach(e => e.parentNode.removeChild(e));
|
||||
document.querySelectorAll(".mobile-footer-spacer").forEach(e => e.parentNode.removeChild(e));
|
||||
document.querySelectorAll(".mobile-footer").forEach(e => e.parentNode.removeChild(e));
|
||||
this.loaded = true;
|
||||
},
|
||||
|
||||
fetchConfig() {
|
||||
axios.get('/api/v0/groups/config')
|
||||
.then(res => {
|
||||
this.config = res.data;
|
||||
this.fetchProfile();
|
||||
});
|
||||
},
|
||||
|
||||
fetchProfile() {
|
||||
axios.get('/api/pixelfed/v1/accounts/verify_credentials')
|
||||
.then(res => {
|
||||
this.profile = res.data;
|
||||
this.init();
|
||||
window._sharedData.curUser = res.data;
|
||||
window.App.util.navatar();
|
||||
})
|
||||
},
|
||||
},
|
||||
|
||||
created() {
|
||||
this.fetchConfig();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.group-joins-component {
|
||||
font-family: var(--font-family-sans-serif);
|
||||
}
|
||||
</style>
|
57
resources/assets/components/GroupNotifications.vue
Normal file
57
resources/assets/components/GroupNotifications.vue
Normal file
|
@ -0,0 +1,57 @@
|
|||
<template>
|
||||
<div class="group-notifications-component">
|
||||
<div class="row border-bottom m-0 p-0">
|
||||
<sidebar />
|
||||
|
||||
<div class="col-12 col-md-9 px-0 mx-0">
|
||||
<loader v-if="!loaded" :loaded="loaded" />
|
||||
|
||||
<template v-else>
|
||||
<div class="px-5 pt-4 pb-2">
|
||||
<h2 class="fw-bold">Group Notifications</h2>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script type="text/javascript">
|
||||
import SidebarComponent from '@/groups/sections/Sidebar.vue';
|
||||
import LoaderComponent from '@/groups/sections/Loader.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
"sidebar": SidebarComponent,
|
||||
"loader": LoaderComponent
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
loaded: false,
|
||||
loadTimeout: undefined,
|
||||
}
|
||||
},
|
||||
|
||||
created() {
|
||||
this.loadTimeout = setTimeout(() => {
|
||||
this.loaded = true;
|
||||
}, 1000);
|
||||
},
|
||||
|
||||
beforeUnmount() {
|
||||
clearTimeout(this.loadTimeout);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.group-notifications-component {
|
||||
font-family: var(--font-family-sans-serif);
|
||||
|
||||
.jumbotron {
|
||||
background-color: #fff;
|
||||
border-radius: 0px;
|
||||
}
|
||||
}
|
||||
</style>
|
1190
resources/assets/components/GroupPage.vue
Normal file
1190
resources/assets/components/GroupPage.vue
Normal file
File diff suppressed because it is too large
Load diff
33
resources/assets/components/GroupPost.vue
Normal file
33
resources/assets/components/GroupPost.vue
Normal file
|
@ -0,0 +1,33 @@
|
|||
<template>
|
||||
<div class="group-status-permalink-component">
|
||||
<div class="row border-bottom m-0 p-0">
|
||||
<sidebar />
|
||||
|
||||
<div class="col-12 col-md-9 px-md-0">
|
||||
<group-feed :group-id="gid" :permalinkMode="true" :permalinkId="sid" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script type="text/javascript">
|
||||
import GroupFeed from '@/groups/GroupFeed.vue';
|
||||
import SidebarComponent from '@/groups/sections/Sidebar.vue';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
gid: {
|
||||
type: String
|
||||
},
|
||||
|
||||
sid: {
|
||||
type: String
|
||||
}
|
||||
},
|
||||
|
||||
components: {
|
||||
"group-feed": GroupFeed,
|
||||
"sidebar": SidebarComponent
|
||||
}
|
||||
}
|
||||
</script>
|
443
resources/assets/components/GroupProfile.vue
Normal file
443
resources/assets/components/GroupProfile.vue
Normal file
|
@ -0,0 +1,443 @@
|
|||
<template>
|
||||
<div class="group-profile-component w-100 h-100">
|
||||
<div v-if="!loaded" class="w-100 h-100">
|
||||
<div class="d-flex justify-content-center align-items-center mt-5">
|
||||
<div class="spinner-border" role="status">
|
||||
<span class="sr-only">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<template v-else>
|
||||
<div class="bg-white mb-3 border-bottom">
|
||||
<div class="container-xl header">
|
||||
<div class="header-jumbotron"></div>
|
||||
|
||||
<div class="header-profile-card">
|
||||
<img :src="profile.avatar" class="avatar" onerror="this.onerror=null;this.src='/storage/avatars/default.png?v=2'">
|
||||
<p class="name">
|
||||
{{ profile.display_name }}
|
||||
</p>
|
||||
<p class="username text-muted">
|
||||
<span v-if="profile.local">@{{ profile.username }}</span>
|
||||
<span v-else>{{ profile.acct }}</span>
|
||||
|
||||
<span v-if="profile.is_admin" class="text-danger ml-1" title="Site administrator" data-toggle="tooltip" data-placement="bottom"><i class="far fa-users-crown"></i></span>
|
||||
</p>
|
||||
</div>
|
||||
<!-- <hr> -->
|
||||
<div class="header-navbar">
|
||||
<div></div>
|
||||
|
||||
<div>
|
||||
<a
|
||||
v-if="currentProfile.id === profile.id"
|
||||
class="btn btn-light font-weight-bold mr-2"
|
||||
href="/settings/home">
|
||||
<i class="fas fa-edit mr-1"></i> Edit Profile
|
||||
</a>
|
||||
|
||||
<a
|
||||
v-if="relationship.following"
|
||||
class="btn btn-primary font-weight-bold mr-2"
|
||||
:href="profile.url">
|
||||
<i class="far fa-comment-alt-dots mr-1"></i> Message
|
||||
</a>
|
||||
|
||||
<a
|
||||
v-if="relationship.following"
|
||||
class="btn btn-light font-weight-bold mr-2"
|
||||
:href="profile.url">
|
||||
<i class="fas fa-user-check mr-1"></i> {{ relationship.followed_by ? 'Friends' : 'Following' }}
|
||||
</a>
|
||||
|
||||
<a
|
||||
v-if="!relationship.following"
|
||||
class="btn btn-light font-weight-bold mr-2"
|
||||
:href="profile.url">
|
||||
<i class="fas fa-user mr-1"></i> View Main Profile
|
||||
</a>
|
||||
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-light font-weight-bold dropdown-toggle" type="button" id="amenu" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<i class="fas fa-ellipsis-h"></i>
|
||||
</button>
|
||||
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="amenu">
|
||||
<a v-if="currentProfile.id != profile.id" class="dropdown-item font-weight-bold" :href="`/i/report?type=user&id=${profile.id}`">Report</a>
|
||||
<a v-if="currentProfile.id == profile.id" class="dropdown-item font-weight-bold" href="#">Leave Group</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-100 h-100 group-profile-feed">
|
||||
<div class="container-xl">
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-5">
|
||||
<div class="card card-body shadow-sm infolet">
|
||||
<h5 class="font-weight-bold mb-3">Intro</h5>
|
||||
<div v-if="!profile.local" class="media mb-3 align-items-center">
|
||||
<div class="media-icon">
|
||||
<i class="far fa-globe" title="User is from a remote server" data-toggle="tooltip" data-placement="bottom"></i>
|
||||
</div>
|
||||
<div class="media-body">
|
||||
Remote member from <strong>{{ profile.acct.split('@')[1] }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <div v-if="profile.note.length" class="media mb-3 align-items-center">
|
||||
<i class="far fa-book-user fa-lg text-lighter mr-3" title="User bio" data-toggle="tooltip" data-placement="bottom"></i>
|
||||
<div class="media-body">
|
||||
<span v-html="profile.note"></span>
|
||||
</div>
|
||||
</div> -->
|
||||
<div class="media align-items-center">
|
||||
<div class="media-icon">
|
||||
<i class="fas fa-users" title="User joined group on this date" data-toggle="tooltip" data-placement="bottom"></i>
|
||||
</div>
|
||||
<div class="media-body">
|
||||
{{ roleTitle }} of <strong>{{ group.name }}</strong> since {{ formatJoinedDate(profile.group?.joined) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="canIntersect" class="card card-body shadow-sm infolet">
|
||||
<h5 class="font-weight-bold mb-3">Things in Common</h5>
|
||||
|
||||
<div v-if="commonIntersects.friends.length" class="media mb-3 align-items-center" v-once>
|
||||
<div class="media-icon">
|
||||
<i class="far fa-user-friends"></i>
|
||||
</div>
|
||||
<div class="media-body">
|
||||
{{ commonIntersects.friends_count }} mutual friend<span v-if="commonIntersects.friends.length > 1">s</span> including
|
||||
<span v-for="(friend, index) in commonIntersects.friends"><a :href="friend.url" class="text-dark font-weight-bold">{{ friend.acct }}</a><span v-if="commonIntersects.friends.length > index + 1">, </span><span v-else> </span></span>
|
||||
<!-- <a href="#" class="text-dark font-weight-bold">dansup</a>, <a href="#" class="text-dark font-weight-bold">admin</a> and 1 other -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- <div class="media mb-3 align-items-center">
|
||||
<div class="media-icon">
|
||||
<i class="fas fa-home"></i>
|
||||
</div>
|
||||
<div class="media-body">
|
||||
Lives in <strong>Canada</strong>
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<div v-if="commonIntersects.groups.length" class="media mb-3 align-items-center">
|
||||
<div class="media-icon">
|
||||
<i class="fas fa-users"></i>
|
||||
</div>
|
||||
<div class="media-body">
|
||||
Also member of <a :href="commonIntersects.groups[0].url" class="text-dark font-weight-bold">{{ commonIntersects.groups[0].name }}</a> and {{ commonIntersects.groups_count }} other groups
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="commonIntersects.topics.length" class="media mb-0 align-items-center">
|
||||
<div class="media-icon">
|
||||
<i class="far fa-thumbs-up fa-lg text-lighter"></i>
|
||||
</div>
|
||||
<div class="media-body">
|
||||
Also interested in topics containing
|
||||
<span v-for="(topic, index) in commonIntersects.topics">
|
||||
<span v-if="commonIntersects.topics.length - 1 == index"> and </span><a :href="topic.url" class="font-weight-bold text-dark">#{{ topic.name }}</a><span v-if="commonIntersects.topics.length > index + 2">, </span>
|
||||
</span> hashtags
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-7">
|
||||
<div class="card card-body shadow-sm">
|
||||
<h5 class="font-weight-bold mb-0">Group Posts</h5>
|
||||
</div>
|
||||
|
||||
<div v-if="feedEmpty" class="pt-5 text-center">
|
||||
<h5>No New Posts</h5>
|
||||
<p>{{ profile.username }} hasn't posted anything yet in <strong>{{ group.name }}</strong>.</p>
|
||||
|
||||
<a :href="group.url" class="font-weight-bold">Go Back</a>
|
||||
</div>
|
||||
|
||||
<div v-if="feedLoaded" class="mt-2">
|
||||
<group-status
|
||||
v-for="(status, index) in feed"
|
||||
:key="'gps:' + status.id"
|
||||
:permalinkMode="true"
|
||||
:showGroupChevron="true"
|
||||
:group="group"
|
||||
:prestatus="status"
|
||||
:profile="profile"
|
||||
:group-id="group.id" />
|
||||
|
||||
<div v-if="feed.length >= 1" :distance="800">
|
||||
<infinite-loading @infinite="infiniteFeed">
|
||||
<div slot="no-more"></div>
|
||||
<div slot="no-results"></div>
|
||||
</infinite-loading>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script type="text/javascript">
|
||||
import GroupStatus from '@/groups/partials/GroupStatus.vue';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
gid: {
|
||||
type: String
|
||||
},
|
||||
|
||||
pid: {
|
||||
type: String
|
||||
}
|
||||
},
|
||||
|
||||
components: {
|
||||
'group-status': GroupStatus
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
loaded: false,
|
||||
currentProfile: {},
|
||||
roleTitle: 'Member',
|
||||
group: {},
|
||||
profile: {},
|
||||
relationship: {
|
||||
following: false,
|
||||
},
|
||||
feed: [],
|
||||
ids: [],
|
||||
feedLoaded: false,
|
||||
feedEmpty: false,
|
||||
page: 1,
|
||||
canIntersect: false,
|
||||
commonIntersects: []
|
||||
}
|
||||
},
|
||||
|
||||
beforeMount() {
|
||||
$('body').css('background-color', '#f0f2f5');
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.fetchGroup();
|
||||
this.$nextTick(() => {
|
||||
$('[data-toggle="tooltip"]').tooltip();
|
||||
});
|
||||
},
|
||||
|
||||
methods: {
|
||||
fetchGroup() {
|
||||
axios.get('/api/v0/groups/' + this.gid)
|
||||
.then(res => {
|
||||
this.group = res.data;
|
||||
})
|
||||
.finally(() => {
|
||||
this.fetchSelfProfile();
|
||||
})
|
||||
},
|
||||
|
||||
fetchSelfProfile() {
|
||||
axios.get('/api/pixelfed/v1/accounts/verify_credentials')
|
||||
.then(res => {
|
||||
this.currentProfile = res.data;
|
||||
})
|
||||
.catch(err => {
|
||||
this.$router.push('/groups/' + this.gid)
|
||||
})
|
||||
.finally(() => {
|
||||
this.fetchProfile();
|
||||
})
|
||||
|
||||
this.$nextTick(() => {
|
||||
$('[data-toggle="tooltip"]').tooltip();
|
||||
});
|
||||
},
|
||||
|
||||
fetchProfile() {
|
||||
axios.get('/api/v0/groups/accounts/' + this.gid + '/' + this.pid)
|
||||
.then(res => {
|
||||
this.profile = res.data;
|
||||
if(res.data.group.role == 'founder') {
|
||||
this.roleTitle = 'Admin';
|
||||
}
|
||||
})
|
||||
|
||||
if(window._sharedData.user.id == this.pid) {
|
||||
this.fetchInitialFeed();
|
||||
return;
|
||||
}
|
||||
|
||||
axios.get('/api/v1/accounts/relationships?id[]=' + this.pid)
|
||||
.then(res => {
|
||||
this.relationship = res.data[0];
|
||||
})
|
||||
.finally(() => {
|
||||
this.fetchInitialFeed();
|
||||
})
|
||||
},
|
||||
fetchInitialFeed() {
|
||||
if(window._sharedData.user && window._sharedData.user.id != this.pid) {
|
||||
this.fetchCommonIntersections();
|
||||
}
|
||||
axios.get(`/api/v0/groups/${this.gid}/user/${this.pid}/feed`)
|
||||
.then(res => {
|
||||
this.feed = res.data.filter(s => {
|
||||
return s.pf_type != 'reply:text' || s.account.id != this.profile.id;
|
||||
});
|
||||
this.feedLoaded = true;
|
||||
this.feedEmpty = this.feed.length == 0;
|
||||
this.page++;
|
||||
this.loaded = true;
|
||||
})
|
||||
.catch(err => {
|
||||
this.$router.push('/groups/' + this.gid);
|
||||
console.log(err)
|
||||
});
|
||||
},
|
||||
|
||||
infiniteFeed($state) {
|
||||
if(this.feed.length == 0) {
|
||||
$state.complete();
|
||||
return;
|
||||
}
|
||||
|
||||
axios.get(`/api/v0/groups/${this.group.id}/user/${this.profile.id}/feed`, {
|
||||
params: {
|
||||
page: this.page
|
||||
},
|
||||
}).then(res => {
|
||||
if (res.data.length) {
|
||||
let data = res.data.filter(s => {
|
||||
return s.pf_type != 'reply:text' || s.account.id != this.profile.id;
|
||||
});
|
||||
let self = this;
|
||||
data.forEach(d => {
|
||||
if(self.ids.indexOf(d.id) == -1) {
|
||||
self.ids.push(d.id);
|
||||
self.feed.push(d);
|
||||
}
|
||||
});
|
||||
$state.loaded();
|
||||
this.page++;
|
||||
} else {
|
||||
$state.complete();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
fetchCommonIntersections() {
|
||||
axios.get('/api/v0/groups/member/intersect/common', {
|
||||
params: {
|
||||
gid: this.gid,
|
||||
pid: this.pid
|
||||
}
|
||||
}).then(res => {
|
||||
this.commonIntersects = res.data;
|
||||
this.canIntersect = res.data.groups.length || res.data.topics.length;
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
formatJoinedDate(ts) {
|
||||
let date = new Date(ts);
|
||||
let options = { year: 'numeric', month: 'long' };
|
||||
let formatter = new Intl.DateTimeFormat('en-US', options);
|
||||
return formatter.format(date);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.group-profile-component {
|
||||
background-color: #f0f2f5;
|
||||
|
||||
.header {
|
||||
&-jumbotron {
|
||||
background-color: #F3F4F6;
|
||||
height: 320px;
|
||||
border-bottom-left-radius: 20px;
|
||||
border-bottom-right-radius: 20px;
|
||||
}
|
||||
|
||||
&-profile-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
.avatar {
|
||||
width: 170px;
|
||||
height: 170px;
|
||||
border-radius: 50%;
|
||||
margin-top: -150px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-size: 30px;
|
||||
line-height: 30px;
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.username {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
&-navbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
height: 60px;
|
||||
border-top: 1px solid #F3F4F6;
|
||||
|
||||
.dropdown {
|
||||
display: inline-block;
|
||||
|
||||
&-toggle:after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.group-profile-feed {
|
||||
min-height: 500px;
|
||||
}
|
||||
|
||||
.infolet {
|
||||
margin-bottom: 1rem;
|
||||
|
||||
.media {
|
||||
&-icon {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 30px;
|
||||
margin-right: 10px;
|
||||
|
||||
i {
|
||||
font-size: 1.1rem;
|
||||
color: #D1D5DB !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.btn-light {
|
||||
border-color: #F3F4F6;
|
||||
}
|
||||
}
|
||||
</style>
|
80
resources/assets/components/Groups.vue
Normal file
80
resources/assets/components/Groups.vue
Normal file
|
@ -0,0 +1,80 @@
|
|||
<template>
|
||||
<div class="group-component">
|
||||
<div v-if="tab === 'home'">
|
||||
<groups-home />
|
||||
</div>
|
||||
|
||||
<div v-if="tab === 'createGroup'">
|
||||
<!-- <div class="col-12 group-component-hero">
|
||||
<h3 class="font-weight-bold">Create Group</h3>
|
||||
<button class="btn btn-outline-primary px-3 rounded-pill font-weight-bold" @click="switchTab('home')">Back to Groups</button>
|
||||
</div> -->
|
||||
|
||||
<create-group />
|
||||
</div>
|
||||
|
||||
<div v-if="tab === 'show'">
|
||||
<group-feed :group-id="groupId" :path="path" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script type="text/javascript">
|
||||
import GroupsHome from '@/groups/GroupsHome.vue';
|
||||
import GroupFeed from '@/groups/GroupFeed.vue';
|
||||
import CreateGroup from '@/groups/CreateGroup.vue';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
groupId: {
|
||||
type: String
|
||||
},
|
||||
|
||||
path: {
|
||||
type: String
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
tab: 'home'
|
||||
}
|
||||
},
|
||||
|
||||
components: {
|
||||
"groups-home": GroupsHome,
|
||||
"create-group": CreateGroup,
|
||||
"group-feed": GroupFeed,
|
||||
},
|
||||
|
||||
mounted() {
|
||||
if(this.groupId) {
|
||||
this.tab = 'show';
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
switchTab(newTab) {
|
||||
this.tab = newTab;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.group-component {
|
||||
&-hero {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1rem;
|
||||
border: 1px solid #dee2e6;
|
||||
border-top: 0;
|
||||
background-color: #fff;
|
||||
|
||||
h3 {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
359
resources/assets/components/groups/CreateGroup.vue
Normal file
359
resources/assets/components/groups/CreateGroup.vue
Normal file
|
@ -0,0 +1,359 @@
|
|||
<template>
|
||||
<div class="create-group-component col-12 col-md-9" style="height: 100vh - 51px !important;overflow:hidden">
|
||||
<div v-if="!hide" class="row h-100 bg-lighter">
|
||||
<div class="col-12 col-md-8 border-left">
|
||||
|
||||
<div class="bg-dark p-5 mx-n3">
|
||||
<p class="h1 font-weight-bold text-light mb-2">Create Group</p>
|
||||
<p class="text-lighter mb-0">Create a new federated Group that is compatible with other Pixelfed and Lemmy servers</p>
|
||||
</div>
|
||||
<div class="px-2 mb-5">
|
||||
<div class="mt-4">
|
||||
<text-input
|
||||
label="Group Name"
|
||||
:value="name"
|
||||
:hasLimit="true"
|
||||
:maxLimit="limit.name.max"
|
||||
placeholder="Add your group name"
|
||||
helpText="Alphanumeric characters only, you can change this later."
|
||||
:largeInput="true"
|
||||
@update="handleUpdate('name', $event)"
|
||||
/>
|
||||
|
||||
<hr>
|
||||
|
||||
<select-input
|
||||
label="Group Type"
|
||||
:value="membership"
|
||||
:categories="membershipCategories"
|
||||
placeholder="Select a type"
|
||||
helpText="Select the membership type, you can change this later."
|
||||
@update="handleUpdate('membership', $event)"
|
||||
/>
|
||||
|
||||
<hr>
|
||||
|
||||
<select-input
|
||||
label="Group Category"
|
||||
:value="category"
|
||||
:categories="categories"
|
||||
placeholder="Select a category"
|
||||
helpText="Choose the most relevant category to improve discovery and visibility"
|
||||
@update="handleUpdate('category', $event)"
|
||||
/>
|
||||
|
||||
<hr>
|
||||
|
||||
<text-area-input
|
||||
label="Group Description"
|
||||
:value="description"
|
||||
:hasLimit="true"
|
||||
:maxLimit="limit.description.max"
|
||||
placeholder="Describe your groups purpose in a few words"
|
||||
helpText="Describe your groups purpose in a few words, you can change this later."
|
||||
@update="handleUpdate('description', $event)"
|
||||
/>
|
||||
|
||||
<hr>
|
||||
|
||||
<checkbox-input
|
||||
label="Adult Content"
|
||||
inputText="Allow Adult Content"
|
||||
:value="configuration.adult"
|
||||
helpText="Groups that allow adult content should enable this or risk suspension or deletion by instance admins. Illegal content is prohibited. You can change this later."
|
||||
/>
|
||||
|
||||
<hr>
|
||||
|
||||
<checkbox-input
|
||||
label=""
|
||||
inputText="I agree to the the Community Guidelines and Terms of Use and will administrate this group according to the rules set by this server. I understand that failure to abide by these terms may lead to the suspension of this group, and my account."
|
||||
:value="hasConfirmed"
|
||||
:strongText="false"
|
||||
@update="handleUpdate('hasConfirmed', $event)"
|
||||
/>
|
||||
|
||||
<!-- <div class="form-group row">
|
||||
<div class="col-sm-10 offset-sm-2">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="gridCheck1" v-model="hasConfirmed">
|
||||
<label class="form-check-label" for="gridCheck1">
|
||||
I agree to the <a href="#">Community Guidelines</a> and <a href="#">Terms of Use</a>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<button
|
||||
class="btn btn-primary btn-block font-weight-bold rounded-pill mt-4"
|
||||
@click="createGroup"
|
||||
:disabled="!hasConfirmed">
|
||||
Create Group
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-4 bg-white">
|
||||
<!-- <div>
|
||||
<button
|
||||
v-if="page <= 4"
|
||||
class="btn btn-primary btn-block font-weight-bold rounded-pill mt-4"
|
||||
@click="nextPage"
|
||||
:disabled="!membership">
|
||||
Next
|
||||
</button>
|
||||
|
||||
<button
|
||||
v-if="page == 5"
|
||||
class="btn btn-primary btn-block font-weight-bold rounded-pill mt-4"
|
||||
@click="createGroup"
|
||||
:disabled="!hasConfirmed">
|
||||
Create Group
|
||||
</button>
|
||||
|
||||
<button
|
||||
v-if="page >= 2"
|
||||
class="btn btn-light btn-block font-weight-bold rounded-pill border mt-4"
|
||||
@click="prevPage">
|
||||
Back
|
||||
</button>
|
||||
|
||||
<div v-if="maxPage > 2" class="mt-4">
|
||||
<p v-if="name && name.length">
|
||||
<span class="text-lighter">Name:</span>
|
||||
<span class="font-weight-bold">{{ name }}</span>
|
||||
</p>
|
||||
|
||||
<p v-if="description && description.length">
|
||||
<span class="text-lighter">Description:</span>
|
||||
<span>{{ description }}</span>
|
||||
</p>
|
||||
|
||||
<p v-if="membership && membership.length">
|
||||
<span class="text-lighter">Membership:</span>
|
||||
<span class="text-capitalize">{{ membership }}</span>
|
||||
</p>
|
||||
|
||||
<p v-if="category && category.length">
|
||||
<span class="text-lighter">Category:</span>
|
||||
<span class="text-capitalize">{{ category }}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div> -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script type="text/javascript">
|
||||
import TextInput from '@/groups/partials/CreateForm/TextInput.vue';
|
||||
import SelectInput from '@/groups/partials/CreateForm/SelectInput.vue';
|
||||
import TextAreaInput from '@/groups/partials/CreateForm/TextAreaInput.vue';
|
||||
import CheckboxInput from '@/groups/partials/CreateForm/CheckboxInput.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
"text-input": TextInput,
|
||||
"select-input": SelectInput,
|
||||
"text-area-input": TextAreaInput,
|
||||
"checkbox-input": CheckboxInput,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
hide: true,
|
||||
name: null,
|
||||
page: 1,
|
||||
maxPage: 1,
|
||||
description: null,
|
||||
membership: "placeholder",
|
||||
submitting: false,
|
||||
categories: [],
|
||||
category: "",
|
||||
limit: {
|
||||
name: {
|
||||
max: 60
|
||||
},
|
||||
description: {
|
||||
max: 500
|
||||
}
|
||||
},
|
||||
configuration: {
|
||||
types: {
|
||||
text: true,
|
||||
photos: true,
|
||||
videos: true,
|
||||
// events: false,
|
||||
polls: true
|
||||
},
|
||||
federation: true,
|
||||
adult: false,
|
||||
discoverable: false,
|
||||
autospam: false,
|
||||
dms: false,
|
||||
slowjoin: {
|
||||
enabled: false,
|
||||
age: 90,
|
||||
limit: {
|
||||
post: 1,
|
||||
comment: 20,
|
||||
threads: 2,
|
||||
likes: 5,
|
||||
hashtags: 5,
|
||||
mentions: 1,
|
||||
autolinks: 1
|
||||
}
|
||||
}
|
||||
},
|
||||
hasConfirmed: false,
|
||||
permissionChecked: false,
|
||||
membershipCategories: [
|
||||
{ key: 'Public', value: 'public' },
|
||||
{ key: 'Private', value: 'private' },
|
||||
{ key: 'Local', value: 'local' },
|
||||
],
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.permissionCheck();
|
||||
this.fetchCategories();
|
||||
},
|
||||
|
||||
methods: {
|
||||
permissionCheck() {
|
||||
axios.post('/api/v0/groups/permission/create')
|
||||
.then(res => {
|
||||
if(res.data.permission == false) {
|
||||
swal('Limit reached', 'You cannot create any more groups', 'error');
|
||||
this.hide = true;
|
||||
} else {
|
||||
this.hide = false;
|
||||
}
|
||||
this.permissionChecked = true;
|
||||
});
|
||||
},
|
||||
|
||||
submit($event) {
|
||||
$event.preventDefault();
|
||||
this.submitting = true;
|
||||
|
||||
axios.post('/api/v0/groups/create', {
|
||||
name: this.name,
|
||||
description: this.description,
|
||||
membership: this.membership
|
||||
}).then(res => {
|
||||
console.log(res.data);
|
||||
window.location.href = res.data.url;
|
||||
}).catch(err => {
|
||||
console.log(err.response);
|
||||
|
||||
})
|
||||
},
|
||||
|
||||
fetchCategories() {
|
||||
axios.get('/api/v0/groups/categories/list')
|
||||
.then(res => {
|
||||
this.categories = res.data.map(c => {
|
||||
return {
|
||||
key: c,
|
||||
value: c
|
||||
}
|
||||
});
|
||||
})
|
||||
},
|
||||
|
||||
createGroup() {
|
||||
axios.post('/api/v0/groups/create', {
|
||||
name: this.name,
|
||||
description: this.description,
|
||||
membership: this.membership,
|
||||
configuration: this.configuration
|
||||
})
|
||||
.then(res => {
|
||||
console.log(res.data);
|
||||
location.href = res.data.url;
|
||||
})
|
||||
},
|
||||
|
||||
handleUpdate(key, val) {
|
||||
this[key] = val;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.create-group-component {
|
||||
.submit-button {
|
||||
width: 130px;
|
||||
}
|
||||
|
||||
.multistep {
|
||||
margin-top: 30px;
|
||||
margin-bottom: 30px;
|
||||
overflow: hidden;
|
||||
counter-reset: step;
|
||||
text-align: center;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.multistep li {
|
||||
list-style-type: none;
|
||||
text-transform: uppercase;
|
||||
font-size: 9px;
|
||||
font-weight: 700;
|
||||
width: 20%;
|
||||
float: left;
|
||||
position: relative;
|
||||
color: #B8C2CC;
|
||||
}
|
||||
|
||||
.multistep li.active {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.multistep li:before {
|
||||
content: counter(step);
|
||||
counter-increment: step;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
line-height: 26px;
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
color: #B8C2CC;
|
||||
background: #F3F4F6;
|
||||
border-radius: 25px;
|
||||
margin: 0 auto 10px auto;
|
||||
transition: background 400ms;
|
||||
}
|
||||
|
||||
.multistep li:after {
|
||||
content: '';
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
background: #dee2e6;
|
||||
position: absolute;
|
||||
left: -50%;
|
||||
top: 11px;
|
||||
z-index: -1;
|
||||
transition: background 400ms;
|
||||
}
|
||||
|
||||
.multistep li:first-child:after {
|
||||
content: none;
|
||||
}
|
||||
|
||||
.multistep li.active:before,
|
||||
.multistep li.active:after {
|
||||
background: #2c78bf;
|
||||
color: white;
|
||||
transition: background 400ms;
|
||||
}
|
||||
|
||||
.col-form-label {
|
||||
font-weight: 600;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
</style>
|
989
resources/assets/components/groups/GroupFeed.vue
Normal file
989
resources/assets/components/groups/GroupFeed.vue
Normal file
|
@ -0,0 +1,989 @@
|
|||
<template>
|
||||
<div class="group-feed-component">
|
||||
<div v-if="!initalLoad">
|
||||
<p class="text-center mt-5 pt-5 font-weight-bold">Loading...</p>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<div class="mb-3 border-bottom">
|
||||
<div class="container-xl">
|
||||
<group-banner
|
||||
:group="group"
|
||||
/>
|
||||
<group-header-details
|
||||
:group="group"
|
||||
:isAdmin="isAdmin"
|
||||
:isMember="isMember"
|
||||
@refresh="handleRefresh"
|
||||
/>
|
||||
<group-nav-tabs
|
||||
:group="group"
|
||||
:isAdmin="isAdmin"
|
||||
:isMember="isMember"
|
||||
:atabs="atabs"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container-xl group-feed-component-body">
|
||||
<div class="row mb-5">
|
||||
<div class="col-12 col-md-7 mt-3">
|
||||
<div v-if="group.self.is_member">
|
||||
<group-compose
|
||||
v-if="initalLoad"
|
||||
:profile="profile"
|
||||
:group-id="groupId"
|
||||
v-on:new-status="pushNewStatus" />
|
||||
|
||||
<div v-if="feed.length == 0" class="mt-3">
|
||||
<div class="card card-body shadow-none border d-flex align-items-center justify-content-center" style="height: 200px;">
|
||||
<p class="font-weight-bold mb-0">No posts yet!</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="group-timeline">
|
||||
<p class="font-weight-bold mb-1">Recent Posts</p>
|
||||
|
||||
|
||||
<!-- <div class="status-card-component shadow-sm mb-3 rounded-lg">
|
||||
<div class="card shadow-none border rounded-0">
|
||||
<div class="card-body pb-0">
|
||||
<div class="media">
|
||||
<img src="https://pixelfed.test/storage/avatars/321493203255693312/5a6nqo.jpg?v=2" width="42px" height="42px" onerror="this.onerror=null;this.src='/storage/avatars/default.png?v=2'" alt="avatar" class="rounded-circle box-shadow mr-2">
|
||||
|
||||
<div class="media-body">
|
||||
<div class="pl-2 d-flex align-items-top">
|
||||
<div>
|
||||
<p class="mb-0">
|
||||
<a href="https://pixelfed.test/dansup" class="username font-weight-bold text-dark text-decoration-none text-break">
|
||||
dansup
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p class="mb-0">
|
||||
<a href="https://pixelfed.test/groups/328821658771132416/329186991407239168" class="font-weight-light text-muted small">13h</a>
|
||||
|
||||
<span class="text-lighter" style="padding-left: 2px; padding-right: 2px;">·</span>
|
||||
|
||||
<span class="text-muted small">
|
||||
<i class="fas fa-globe"></i>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<span class="text-right" style="flex-grow: 1;">
|
||||
<button type="button" class="btn btn-link text-dark py-0">
|
||||
<span class="fas fa-ellipsis-h text-lighter"></span>
|
||||
|
||||
<span class="sr-only">Post Menu</span>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div>
|
||||
<div class="">
|
||||
<p class="pt-2 text-break" style="font-size: 15px;">
|
||||
Made some improvements!
|
||||
</p>
|
||||
|
||||
<div class="my-3 row px-0 mx-0 card card-body my-0 py-0 border shadow-none">
|
||||
<img src="https://opengraph.githubassets.com/f66d0f7bf17df4a45382b83c1ffde2f25e3d700f9d87ab8c9ec2029c3a1e16b6/pixelfed/pixelfed/pull/2865" class="img-fluid">
|
||||
<div class="bg-light px-3 pt-2 pb-3">
|
||||
<p class="text-muted mb-0 small">GITHUB.COM</p>
|
||||
<p class="mb-0" style="font-size: 16px;font-weight:500;">Update LikeController, add UndoLikePipeline and federate Undo Like ac… by dansup · Pull Request #2865 · pixelfed/pixelfed</p>
|
||||
<p class="mb-0 text-muted" style="font-size:14px;line-height:15px;">…tivities</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="border-top my-0">
|
||||
<div class="d-flex justify-content-between py-2 px-4">
|
||||
<button class="btn btn-link py-0 text-decoration-none text-muted">
|
||||
<i class="far fa-heart mr-1"></i>
|
||||
Like
|
||||
</button>
|
||||
|
||||
<div>
|
||||
<i class="far fa-comment cursor-pointer text-muted mr-1"></i>
|
||||
Comment
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<i class="fas fa-external-link-alt cursor-pointer text-muted mr-1"></i>
|
||||
Share
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<!-- OGP -->
|
||||
<!-- <div class="status-card-component shadow-sm mb-3 rounded-lg">
|
||||
<div class="card shadow-none border rounded-0">
|
||||
<div class="card-body pb-0">
|
||||
<div class="media">
|
||||
<img src="https://pixelfed.test/storage/avatars/321493203255693312/5a6nqo.jpg?v=2" width="42px" height="42px" onerror="this.onerror=null;this.src='/storage/avatars/default.png?v=2'" alt="avatar" class="rounded-circle box-shadow mr-2">
|
||||
|
||||
<div class="media-body">
|
||||
<div class="pl-2 d-flex align-items-top">
|
||||
<div>
|
||||
<p class="mb-0">
|
||||
<a href="https://pixelfed.test/dansup" class="username font-weight-bold text-dark text-decoration-none text-break">
|
||||
dansup
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p class="mb-0">
|
||||
<a href="https://pixelfed.test/groups/328821658771132416/329186991407239168" class="font-weight-light text-muted small">13h</a>
|
||||
|
||||
<span class="text-lighter" style="padding-left: 2px; padding-right: 2px;">·</span>
|
||||
|
||||
<span class="text-muted small">
|
||||
<i class="fas fa-globe"></i>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<span class="text-right" style="flex-grow: 1;">
|
||||
<button type="button" class="btn btn-link text-dark py-0">
|
||||
<span class="fas fa-ellipsis-h text-lighter"></span>
|
||||
|
||||
<span class="sr-only">Post Menu</span>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div>
|
||||
<div class="">
|
||||
<div class="my-3 row px-0 mx-0 card card-body my-0 py-0 border shadow-none">
|
||||
<img src="https://www.ctvnews.ca/polopoly_fs/1.5533318.1628281952!/httpImage/image.jpg_gen/derivatives/landscape_620/image.jpg" class="img-fluid">
|
||||
<div class="bg-light px-3 pt-2 pb-3">
|
||||
<p class="text-muted mb-0 small">CTVNEWS.CA</p>
|
||||
<p class="mb-0" style="font-size: 16px;font-weight:500;">No charges against Alberta man who fatally shot home intruder: RCMP</p>
|
||||
<p class="mb-0 text-muted" style="font-size:14px;line-height:15px;">No charges will be laid against an Alberta man who shot and killed an intruder after being beaten with a baseball bat, RCMP announced Friday.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="border-top my-0">
|
||||
<div class="d-flex justify-content-between py-2 px-4">
|
||||
<button class="btn btn-link py-0 text-decoration-none text-muted">
|
||||
<i class="far fa-heart mr-1"></i>
|
||||
Like
|
||||
</button>
|
||||
|
||||
<div>
|
||||
<i class="far fa-comment cursor-pointer text-muted mr-1"></i>
|
||||
Comment
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<i class="fas fa-external-link-alt cursor-pointer text-muted mr-1"></i>
|
||||
Share
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<!-- SOUNDCLOUD -->
|
||||
<!-- <div class="status-card-component shadow-sm mb-3 rounded-lg">
|
||||
<div class="card shadow-none border rounded-0">
|
||||
<div class="card-body pb-0">
|
||||
<div class="media">
|
||||
<img src="https://pixelfed.test/storage/avatars/321493203255693312/5a6nqo.jpg?v=2" width="42px" height="42px" onerror="this.onerror=null;this.src='/storage/avatars/default.png?v=2'" alt="avatar" class="rounded-circle box-shadow mr-2">
|
||||
|
||||
<div class="media-body">
|
||||
<div class="pl-2 d-flex align-items-top">
|
||||
<div>
|
||||
<p class="mb-0">
|
||||
<a href="https://pixelfed.test/dansup" class="username font-weight-bold text-dark text-decoration-none text-break">
|
||||
dansup
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p class="mb-0">
|
||||
<a href="https://pixelfed.test/groups/328821658771132416/329186991407239168" class="font-weight-light text-muted small">13h</a>
|
||||
|
||||
<span class="text-lighter" style="padding-left: 2px; padding-right: 2px;">·</span>
|
||||
|
||||
<span class="text-muted small">
|
||||
<i class="fas fa-globe"></i>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<span class="text-right" style="flex-grow: 1;">
|
||||
<button type="button" class="btn btn-link text-dark py-0">
|
||||
<span class="fas fa-ellipsis-h text-lighter"></span>
|
||||
|
||||
<span class="sr-only">Post Menu</span>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div>
|
||||
<div class="">
|
||||
<p class="pt-2 text-break" style="font-size: 15px;">
|
||||
What does everyone think??
|
||||
</p>
|
||||
|
||||
<div class="my-3 row p-0 m-0">
|
||||
<iframe width="100%" height="166" scrolling="no" frameborder="no" allow="autoplay" src="https://w.soundcloud.com/player/?url=https%3A//api.soundcloud.com/tracks/34019569&color=0066cc"></iframe><div style="font-size: 10px; color: #cccccc;line-break: anywhere;word-break: normal;overflow: hidden;white-space: nowrap;text-overflow: ellipsis; font-family: Interstate,Lucida Grande,Lucida Sans Unicode,Lucida Sans,Garuda,Verdana,Tahoma,sans-serif;font-weight: 100;"><a href="https://soundcloud.com/the-bugle" title="The Bugle" target="_blank" style="color: #cccccc; text-decoration: none;">The Bugle</a> · <a href="https://soundcloud.com/the-bugle/bugle-179-playas-gon-play" title="Bugle 179 - Playas gon play" target="_blank" style="color: #cccccc; text-decoration: none;">Bugle 179 - Playas gon play</a></div>
|
||||
</div>
|
||||
|
||||
<div class="border-top my-0">
|
||||
<div class="d-flex justify-content-between py-2 px-4">
|
||||
<button class="btn btn-link py-0 text-decoration-none text-muted">
|
||||
<i class="far fa-heart mr-1"></i>
|
||||
Like
|
||||
</button>
|
||||
|
||||
<div>
|
||||
<i class="far fa-comment cursor-pointer text-muted mr-1"></i>
|
||||
Comment
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<i class="fas fa-external-link-alt cursor-pointer text-muted mr-1"></i>
|
||||
Share
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<!-- YOUTUBE -->
|
||||
<!-- <div class="status-card-component shadow-sm mb-3 rounded-lg">
|
||||
<div class="card shadow-none border rounded-0">
|
||||
<div class="card-body pb-0">
|
||||
<div class="media">
|
||||
<img src="https://pixelfed.test/storage/avatars/321493203255693312/5a6nqo.jpg?v=2" width="42px" height="42px" onerror="this.onerror=null;this.src='/storage/avatars/default.png?v=2'" alt="avatar" class="rounded-circle box-shadow mr-2">
|
||||
|
||||
<div class="media-body">
|
||||
<div class="pl-2 d-flex align-items-top">
|
||||
<div>
|
||||
<p class="mb-0">
|
||||
<a href="https://pixelfed.test/dansup" class="username font-weight-bold text-dark text-decoration-none text-break">
|
||||
dansup
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p class="mb-0">
|
||||
<a href="https://pixelfed.test/groups/328821658771132416/329186991407239168" class="font-weight-light text-muted small">13h</a>
|
||||
|
||||
<span class="text-lighter" style="padding-left: 2px; padding-right: 2px;">·</span>
|
||||
|
||||
<span class="text-muted small">
|
||||
<i class="fas fa-globe"></i>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<span class="text-right" style="flex-grow: 1;">
|
||||
<button type="button" class="btn btn-link text-dark py-0">
|
||||
<span class="fas fa-ellipsis-h text-lighter"></span>
|
||||
|
||||
<span class="sr-only">Post Menu</span>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div>
|
||||
<div class="">
|
||||
<p class="pt-2 text-break" style="font-size: 15px;">
|
||||
What does everyone think??
|
||||
</p>
|
||||
|
||||
<div class="my-3 row p-0 m-0">
|
||||
<iframe width="100%" height="315" src="https://www.youtube.com/embed/lH78Tb0r_f8" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
|
||||
</div>
|
||||
|
||||
<div class="border-top my-0">
|
||||
<div class="d-flex justify-content-between py-2 px-4">
|
||||
<button class="btn btn-link py-0 text-decoration-none text-muted">
|
||||
<i class="far fa-heart mr-1"></i>
|
||||
Like
|
||||
</button>
|
||||
|
||||
<div>
|
||||
<i class="far fa-comment cursor-pointer text-muted mr-1"></i>
|
||||
Comment
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<i class="fas fa-external-link-alt cursor-pointer text-muted mr-1"></i>
|
||||
Share
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<!-- PHOTOS -->
|
||||
<!-- <div class="status-card-component shadow-sm mb-3 rounded-lg">
|
||||
<div class="card shadow-none border rounded-0">
|
||||
<div class="card-body pb-0">
|
||||
<div class="media">
|
||||
<img src="https://pixelfed.test/storage/avatars/321493203255693312/5a6nqo.jpg?v=2" width="42px" height="42px" onerror="this.onerror=null;this.src='/storage/avatars/default.png?v=2'" alt="avatar" class="rounded-circle box-shadow mr-2">
|
||||
|
||||
<div class="media-body">
|
||||
<div class="pl-2 d-flex align-items-top">
|
||||
<div>
|
||||
<p class="mb-0">
|
||||
<a href="https://pixelfed.test/dansup" class="username font-weight-bold text-dark text-decoration-none text-break">
|
||||
dansup
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p class="mb-0">
|
||||
<a href="https://pixelfed.test/groups/328821658771132416/329186991407239168" class="font-weight-light text-muted small">13h</a>
|
||||
|
||||
<span class="text-lighter" style="padding-left: 2px; padding-right: 2px;">·</span>
|
||||
|
||||
<span class="text-muted small">
|
||||
<i class="fas fa-globe"></i>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<span class="text-right" style="flex-grow: 1;">
|
||||
<button type="button" class="btn btn-link text-dark py-0">
|
||||
<span class="fas fa-ellipsis-h text-lighter"></span>
|
||||
|
||||
<span class="sr-only">Post Menu</span>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div>
|
||||
<div class="">
|
||||
<p class="pt-2 text-break" style="font-size: 15px;">
|
||||
What does everyone think??
|
||||
</p>
|
||||
|
||||
<div class="mb-1 row px-3">
|
||||
<div class="col px-0">
|
||||
<img src="https://pixelfed.test/img/sample-post.jpeg" class="img-fluid border rounded-lg">
|
||||
</div>
|
||||
<div class="col px-0">
|
||||
<img src="https://pixelfed.test/img/sample-post.jpeg" class="img-fluid border rounded-lg">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3 row px-3">
|
||||
<div class="col px-0">
|
||||
<img src="https://pixelfed.test/img/sample-post.jpeg" class="img-fluid border rounded-lg">
|
||||
</div>
|
||||
<div class="col px-0">
|
||||
<img src="https://pixelfed.test/img/sample-post.jpeg" class="img-fluid border rounded-lg">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="border-top my-0">
|
||||
<div class="d-flex justify-content-between py-2 px-4">
|
||||
<button class="btn btn-link py-0 text-decoration-none text-muted">
|
||||
<i class="far fa-heart mr-1"></i>
|
||||
Like
|
||||
</button>
|
||||
|
||||
<div>
|
||||
<i class="far fa-comment cursor-pointer text-muted mr-1"></i>
|
||||
Comment
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<i class="fas fa-external-link-alt cursor-pointer text-muted mr-1"></i>
|
||||
Share
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<group-status
|
||||
v-for="(status, index) in feed"
|
||||
:key="'gs:' + status.id + index"
|
||||
:prestatus="status"
|
||||
:profile="profile"
|
||||
:group-id="groupId"
|
||||
v-on:comment-focus="commentFocus(index)"
|
||||
v-on:status-delete="statusDelete(index)"
|
||||
v-on:likes-modal="showLikesModal(index)" />
|
||||
|
||||
<b-modal
|
||||
ref="likeBox"
|
||||
size="sm"
|
||||
centered
|
||||
hide-footer
|
||||
title="Likes"
|
||||
body-class="list-group-flush p-0">
|
||||
<div class="list-group py-1" style="max-height:300px;overflow-y:auto;">
|
||||
<div
|
||||
class="list-group-item border-top-0 border-left-0 border-right-0 py-2"
|
||||
:class="{ 'border-bottom-0': index + 1 == likes.length }"
|
||||
v-for="(user, index) in likes" :key="'modal_likes_'+index">
|
||||
<div class="media align-items-center">
|
||||
<a :href="user.url">
|
||||
<img class="mr-3 rounded-circle box-shadow" :src="user.avatar" :alt="user.username + '’s avatar'" width="30px" onerror="this.onerror=null;this.src='/storage/avatars/default.jpg';">
|
||||
</a>
|
||||
<div class="media-body">
|
||||
<p class="mb-0" style="font-size: 14px">
|
||||
<a :href="user.url" class="font-weight-bold text-dark">
|
||||
{{user.username}}
|
||||
</a>
|
||||
</p>
|
||||
<p v-if="!user.local" class="text-muted mb-0 text-truncate mr-3" style="font-size: 14px" :title="user.acct" data-toggle="dropdown" data-placement="bottom">
|
||||
<span class="font-weight-bold">{{user.acct.split('@')[0]}}</span><span class="text-lighter">@{{user.acct.split('@')[1]}}</span>
|
||||
</p>
|
||||
<p v-else class="text-muted mb-0 text-truncate" style="font-size: 14px">
|
||||
{{user.display_name}}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<infinite-loading @infinite="infiniteLikesHandler" :distance="800" spinner="spiral">
|
||||
<div slot="no-more"></div>
|
||||
<div slot="no-results"></div>
|
||||
</infinite-loading>
|
||||
</div>
|
||||
</b-modal>
|
||||
|
||||
<div v-if="feed.length > 2" :distance="800">
|
||||
<infinite-loading @infinite="infiniteFeed">
|
||||
<div slot="no-more"></div>
|
||||
<div slot="no-results"></div>
|
||||
</infinite-loading>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<div class="card card-body mt-3 shadow-none border d-flex align-items-center justify-content-center" style="height: 100px;">
|
||||
<p class="lead mb-0">Join to participate in this group.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-5">
|
||||
<group-info-card :group="group" />
|
||||
</div>
|
||||
</div>
|
||||
<search-modal ref="searchModal" :group="group" :profile="profile" />
|
||||
<invite-modal ref="inviteModal" :group="group" :profile="profile" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script type="text/javascript">
|
||||
import StatusCard from '~/partials/StatusCard.vue';
|
||||
import GroupCompose from './partials/GroupCompose.vue';
|
||||
import GroupStatus from './partials/GroupStatus.vue';
|
||||
import GroupInfoCard from './partials/GroupInfoCard.vue';
|
||||
import LeaveGroup from './partials/LeaveGroup.vue';
|
||||
import SearchModal from './partials/GroupSearchModal.vue';
|
||||
import InviteModal from './partials/GroupInviteModal.vue';
|
||||
import GroupBanner from '@/groups/partials/Page/GroupBanner.vue';
|
||||
import GroupNavTabs from '@/groups/partials/Page/GroupNavTabs.vue';
|
||||
import GroupHeaderDetails from '@/groups/partials/Page/GroupHeaderDetails.vue';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
groupId: {
|
||||
type: String
|
||||
},
|
||||
|
||||
path: {
|
||||
type: String
|
||||
},
|
||||
|
||||
permalinkMode: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
|
||||
permalinkId: {
|
||||
type: String,
|
||||
}
|
||||
},
|
||||
|
||||
components: {
|
||||
'status-card': StatusCard,
|
||||
'group-status': GroupStatus,
|
||||
'group-compose': GroupCompose,
|
||||
'group-info-card': GroupInfoCard,
|
||||
'leave-group': LeaveGroup,
|
||||
'search-modal': SearchModal,
|
||||
'invite-modal': InviteModal,
|
||||
'group-banner': GroupBanner,
|
||||
'group-header-details': GroupHeaderDetails,
|
||||
'group-nav-tabs': GroupNavTabs,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
initalLoad: false,
|
||||
profile: undefined,
|
||||
group: {},
|
||||
isMember: false,
|
||||
isAdmin: false,
|
||||
tab: 'feed',
|
||||
requestingMembership: false,
|
||||
composeText: null,
|
||||
feed: [],
|
||||
ids: [],
|
||||
maxId: null,
|
||||
status: undefined,
|
||||
likes: [],
|
||||
likesPage: 1,
|
||||
likesId: undefined,
|
||||
renderIdx: 1,
|
||||
atabs: {
|
||||
moderation_count: 0,
|
||||
request_count: 0
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
created() {
|
||||
this.fetchSelf();
|
||||
},
|
||||
|
||||
methods: {
|
||||
fetchSelf() {
|
||||
axios.get('/api/v1/accounts/verify_credentials?_pe=1')
|
||||
.then(res => {
|
||||
this.profile = res.data;
|
||||
})
|
||||
.catch(err => {
|
||||
window.location.href = '/login?_next=' + encodeURIComponent(window.location.href);
|
||||
})
|
||||
.finally(() => {
|
||||
this.fetchGroup();
|
||||
});
|
||||
},
|
||||
|
||||
initObservers() {
|
||||
// let video = document.querySelectorAll('video');
|
||||
// let isPaused = false;
|
||||
// let observer = new IntersectionObserver((entries, observer) => {
|
||||
// entries.forEach(entry => {
|
||||
// if (entry.intersectionRatio !=1 && !video.paused){
|
||||
// video.pause();
|
||||
// isPaused = true;
|
||||
// }
|
||||
// else if (isPaused) {
|
||||
// video.play();
|
||||
// isPaused = false
|
||||
// }
|
||||
// });
|
||||
// }, {threshold: 1});
|
||||
// observer.observe(video);
|
||||
},
|
||||
|
||||
fetchGroup() {
|
||||
axios.get('/api/v0/groups/' + this.groupId)
|
||||
.then(res => {
|
||||
this.group = res.data;
|
||||
this.isMember = res.data.self.is_member;
|
||||
this.isAdmin = ['founder', 'admin'].includes(res.data.self.role);
|
||||
|
||||
if(this.isAdmin) {
|
||||
this.fetchAdminTabs();
|
||||
}
|
||||
|
||||
if(this.path) {
|
||||
if(this.isMember && ['about', 'topics', 'members', 'events', 'media', 'polls'].includes(this.path)) {
|
||||
setTimeout(() => {
|
||||
this.tab = this.path;
|
||||
this.initalLoad = true;
|
||||
}, 500);
|
||||
} else if (this.isAdmin && ['insights', 'moderation'].includes(this.path)) {
|
||||
setTimeout(() => {
|
||||
this.tab = this.path;
|
||||
this.initalLoad = true;
|
||||
}, 500);
|
||||
} else {
|
||||
history.pushState(null, null, this.group.url);
|
||||
this.initalLoad = true;
|
||||
}
|
||||
} else {
|
||||
this.initalLoad = true;
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
window.location.href = '/groups/unavailable';
|
||||
})
|
||||
.finally(() => {
|
||||
this.fetchFeed();
|
||||
});
|
||||
},
|
||||
|
||||
fetchAdminTabs() {
|
||||
axios.get('/api/v0/groups/' + this.groupId + '/atabs')
|
||||
.then(res => {
|
||||
this.atabs = res.data;
|
||||
})
|
||||
},
|
||||
|
||||
fetchFeed() {
|
||||
axios.get('/api/v0/groups/' + this.groupId + '/feed')
|
||||
.then(res => {
|
||||
let self = this;
|
||||
if(res.data && res.data.length) {
|
||||
this.feed = res.data;
|
||||
|
||||
this.maxId = this.feed[this.feed.length - 1].id;
|
||||
res.data.forEach(d => {
|
||||
if(self.ids.indexOf(d.id) == -1) {
|
||||
self.ids.push(d.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
this.initObservers();
|
||||
})
|
||||
},
|
||||
|
||||
fetchPermalink() {
|
||||
axios.get('/api/v0/groups/status', {
|
||||
params: {
|
||||
gid: this.groupId,
|
||||
sid: this.permalinkId
|
||||
}
|
||||
}).then(res => {
|
||||
this.status = res.data;
|
||||
if(this.status.in_reply_to_id) {
|
||||
this.status.showCommentDrawer = true;
|
||||
}
|
||||
}).catch(err => {
|
||||
this.permalinkMode = false;
|
||||
this.fetchFeed();
|
||||
});
|
||||
},
|
||||
|
||||
handleRefresh() {
|
||||
this.initialLoad = false;
|
||||
this.init();
|
||||
this.renderIdx++;
|
||||
},
|
||||
|
||||
timestampFormat(date, showTime = false) {
|
||||
let ts = new Date(date);
|
||||
return showTime ? ts.toDateString() + ' · ' + ts.toLocaleTimeString() : ts.toDateString();
|
||||
},
|
||||
|
||||
switchTab(tab) {
|
||||
window.scrollTo(0,0);
|
||||
if(tab == 'feed' && this.permalinkMode) {
|
||||
this.permalinkMode = false;
|
||||
this.fetchFeed();
|
||||
}
|
||||
let url = tab == 'feed' ? this.group.url : this.group.url + '/' + tab;
|
||||
history.pushState(tab, null, url);
|
||||
this.tab = tab;
|
||||
},
|
||||
|
||||
joinGroup() {
|
||||
this.requestingMembership = true;
|
||||
|
||||
axios.post('/api/v0/groups/'+this.groupId+'/join')
|
||||
.then(res => {
|
||||
this.requestingMembership = false;
|
||||
this.group = res.data;
|
||||
this.fetchGroup();
|
||||
this.fetchFeed();
|
||||
}).catch(err => {
|
||||
let body = err.response;
|
||||
|
||||
if(body.status == 422) {
|
||||
this.tab = 'feed';
|
||||
history.pushState('', null, this.group.url);
|
||||
this.requestingMembership = false;
|
||||
swal('Oops!', body.data.error, 'error');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
cancelJoinRequest() {
|
||||
if(!window.confirm('Are you sure you want to cancel your request to join this group?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
axios.post('/api/v0/groups/'+this.groupId+'/cjr')
|
||||
.then(res => {
|
||||
this.requestingMembership = false;
|
||||
}).catch(err => {
|
||||
let body = err.response;
|
||||
|
||||
if(body.status == 422) {
|
||||
swal('Oops!', body.data.error, 'error');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
leaveGroup() {
|
||||
if(!window.confirm('Are you sure you want to leave this group? Any content you shared will remain accessible. You won\'t be able to rejoin for 24 hours.')) {
|
||||
return;
|
||||
}
|
||||
|
||||
axios.post('/api/v0/groups/'+this.groupId+'/leave')
|
||||
.then(res => {
|
||||
this.tab = 'feed';
|
||||
history.pushState('', null, this.group.url);
|
||||
this.feed = [];
|
||||
this.isMember = false;
|
||||
this.isAdmin = false;
|
||||
this.group.self.role = null;
|
||||
this.group.self.is_member = false;
|
||||
});
|
||||
},
|
||||
|
||||
pushNewStatus(status) {
|
||||
this.feed.unshift(status);
|
||||
},
|
||||
|
||||
commentFocus(index) {
|
||||
let status = this.feed[index];
|
||||
status.showCommentDrawer = true;
|
||||
},
|
||||
|
||||
statusDelete(index) {
|
||||
this.feed.splice(index, 1);
|
||||
},
|
||||
|
||||
infiniteFeed($state) {
|
||||
if(this.feed.length < 3) {
|
||||
$state.complete();
|
||||
return;
|
||||
}
|
||||
let apiUrl = '/api/v0/groups/' + this.groupId + '/feed';
|
||||
axios.get(apiUrl, {
|
||||
params: {
|
||||
limit: 6,
|
||||
max_id: this.maxId
|
||||
},
|
||||
}).then(res => {
|
||||
if (res.data.length) {
|
||||
// let self = this;
|
||||
// data.forEach(d => {
|
||||
// if(self.ids.indexOf(d.id) == -1) {
|
||||
// if(self.maxId >= d.id) {
|
||||
// self.maxId = d.id;
|
||||
// }
|
||||
// self.ids.push(d.id);
|
||||
// self.feed.push(d);
|
||||
// }
|
||||
// });
|
||||
let posts = res.data.filter(p => this.ids.indexOf(p.id) == -1);
|
||||
this.maxId = posts[posts.length - 1].id;
|
||||
this.feed.push(...posts);
|
||||
this.ids.push(...posts.map(p => p.id));
|
||||
setTimeout(() => {
|
||||
this.initObservers();
|
||||
}, 1000);
|
||||
$state.loaded();
|
||||
} else {
|
||||
$state.complete();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
decrementModCounter(amount) {
|
||||
let count = this.atabs.moderation_count;
|
||||
if(count == 0) {
|
||||
return;
|
||||
}
|
||||
this.atabs.moderation_count = (count - amount);
|
||||
},
|
||||
|
||||
setModCounter(amount) {
|
||||
this.atabs.moderation_count = amount;
|
||||
},
|
||||
|
||||
decrementJoinRequestCount(amount = 1) {
|
||||
let count = this.atabs.request_count;
|
||||
this.atabs.request_count = (count - amount)
|
||||
},
|
||||
|
||||
incrementMemberCount() {
|
||||
let count = this.group.member_count;
|
||||
this.group.member_count = (count + 1);
|
||||
},
|
||||
|
||||
copyLink() {
|
||||
window.App.util.clipboard(this.group.url);
|
||||
this.$bvToast.toast(`Succesfully copied group url to clipboard`, {
|
||||
title: 'Success',
|
||||
variant: 'success',
|
||||
autoHideDelay: 5000
|
||||
});
|
||||
},
|
||||
|
||||
reportGroup() {
|
||||
swal('Report Group', 'Are you sure you want to report this group?')
|
||||
.then(res => {
|
||||
if(res) {
|
||||
location.href = `/i/report?id=${this.group.id}&type=group`;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
showSearchModal() {
|
||||
event.currentTarget.blur();
|
||||
this.$refs.searchModal.open();
|
||||
},
|
||||
|
||||
showInviteModal() {
|
||||
event.currentTarget.blur();
|
||||
this.$refs.inviteModal.open();
|
||||
},
|
||||
|
||||
showLikesModal(index) {
|
||||
this.likesId = this.feed[index].id;
|
||||
axios.get('/api/v0/groups/'+this.groupId+'/likes/'+this.likesId)
|
||||
.then(res => {
|
||||
this.likes = res.data;
|
||||
this.likesPage++;
|
||||
this.$refs.likeBox.show();
|
||||
});
|
||||
},
|
||||
|
||||
infiniteLikesHandler($state) {
|
||||
if(this.likes.length < 3) {
|
||||
$state.complete();
|
||||
return;
|
||||
}
|
||||
axios.get('/api/v0/groups/'+this.groupId+'/likes/'+this.likesId, {
|
||||
params: {
|
||||
page: this.likesPage,
|
||||
},
|
||||
}).then(res => {
|
||||
if (res.data.length > 0) {
|
||||
this.likes.push(...res.data);
|
||||
this.likesPage++;
|
||||
if(res.data.length != 10) {
|
||||
$state.complete();
|
||||
} else {
|
||||
$state.loaded();
|
||||
}
|
||||
} else {
|
||||
$state.complete();
|
||||
}
|
||||
});
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.group-feed-component {
|
||||
&-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-end;
|
||||
padding: 1rem 0;
|
||||
background-color: transparent;
|
||||
|
||||
.cta-btn {
|
||||
width: 190px;
|
||||
}
|
||||
}
|
||||
|
||||
.header-jumbotron {
|
||||
background-color: #F3F4F6;
|
||||
height: 320px;
|
||||
border-bottom-left-radius: 20px;
|
||||
border-bottom-right-radius: 20px;
|
||||
}
|
||||
|
||||
&-menu {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0;
|
||||
|
||||
&-nav {
|
||||
.nav-item {
|
||||
.nav-link {
|
||||
padding-top: 1rem;
|
||||
padding-bottom: 1rem;
|
||||
color: #6c757d;
|
||||
|
||||
&.active {
|
||||
color: #2c78bf;
|
||||
border-bottom: 2px solid #2c78bf;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:not(last-child) {
|
||||
.nav-item {
|
||||
margin-right: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-body {
|
||||
min-height: 40vh;
|
||||
}
|
||||
|
||||
.member-label {
|
||||
padding: 2px 5px;
|
||||
font-size: 12px;
|
||||
color: rgba(75, 119, 190, 1);
|
||||
background:rgba(137, 196, 244, 0.2);
|
||||
border:1px solid rgba(137, 196, 244, 0.3);
|
||||
font-weight:400;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.remote-label {
|
||||
padding: 2px 5px;
|
||||
font-size: 12px;
|
||||
color: #B45309;
|
||||
background: #FEF3C7;
|
||||
border: 1px solid #FCD34D;
|
||||
font-weight: 400;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
}
|
||||
</style>
|
217
resources/assets/components/groups/GroupInvite.vue
Normal file
217
resources/assets/components/groups/GroupInvite.vue
Normal file
|
@ -0,0 +1,217 @@
|
|||
<template>
|
||||
<div class="group-invite-component">
|
||||
<div class="container">
|
||||
<div class="row justify-content-center mt-5">
|
||||
<div class="col-12 col-md-7">
|
||||
<div class="card shadow-none border" style="min-height: 300px;">
|
||||
<div class="card-body d-flex justify-content-center align-items-center">
|
||||
|
||||
<transition-group name="fade">
|
||||
<div v-if="tab === 'initial'" key="initial">
|
||||
<p class="text-center mb-1"><b-spinner variant="lighter" /></p>
|
||||
<p class="text-center small text-muted mb-0">{{ loadingStatus }}</p>
|
||||
</div>
|
||||
|
||||
<div v-else-if="tab === 'loading'" key="loading">
|
||||
<p class="text-center mb-1"><b-spinner variant="lighter" /></p>
|
||||
</div>
|
||||
|
||||
<div v-else-if="tab === 'login'" key="login">
|
||||
<p class="text-center mb-0">Please <a href="/login">login</a> to continue</p>
|
||||
</div>
|
||||
|
||||
<div v-else-if="tab === 'form'" key="form">
|
||||
<div class="d-flex justify-content-center align-items-center flex-column">
|
||||
<p class="text-center h4 font-weight-bold"><a href="#">@dansup</a> invited you to join</p>
|
||||
|
||||
<div class="card my-3 shadow-none border" style="width: 300px;">
|
||||
<img v-if="group.metadata && group.metadata.hasOwnProperty('header')" :src="group.metadata.header.url" class="card-img-top" style="width: 100%; height: 100px;object-fit: cover;">
|
||||
<div v-else class="card-img-top" style="width: 100px; height: 100px;padding: 5px;">
|
||||
<div class="bg-primary d-flex align-items-center justify-content-center" style="width: 100%; height:100%;">
|
||||
<i class="fal fa-users text-white fa-lg"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<p class="h5 font-weight-bold mb-1 text-dark">
|
||||
{{ group.name || 'Untitled Group' }}
|
||||
</p>
|
||||
|
||||
<transition name="fade">
|
||||
<p v-if="showMore" class="text-muted small mb-1">
|
||||
{{ group.description }}
|
||||
</p>
|
||||
</transition>
|
||||
|
||||
<p class="mb-1">
|
||||
<span class="text-muted mr-2">
|
||||
<i class="far fa-users fa-sm text-lighter mr-1"></i>
|
||||
<span class="small font-weight-bold">{{ prettyCount(group.member_count) }} Members</span>
|
||||
</span>
|
||||
|
||||
<span v-if="!group.local" class="remote-label ml-2">
|
||||
<i class="fal fa-globe"></i> Remote
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<transition name="fade">
|
||||
<div v-if="showMore">
|
||||
<p class="text-muted small mb-1">
|
||||
<i class="far fa-tag fa-sm text-lighter mr-2"></i>
|
||||
<span class="font-weight-bold">Category: {{ group.category.name }}</span>
|
||||
</p>
|
||||
<p class="text-muted small mb-1">
|
||||
<i class="far fa-clock fa-sm text-lighter mr-2"></i>
|
||||
<span class="font-weight-bold">Created {{ timeago(group.created_at) }} ago</span>
|
||||
</p>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="d-flex justify-content-between">
|
||||
<button class="btn btn-light border-lighter font-weight-bold btn-sm" @click="showMoreInfo">
|
||||
{{ showMore ? 'Less' :'More' }} info
|
||||
</button>
|
||||
<div>
|
||||
<button class="btn btn-light font-weight-bold btn-sm" @click="declineInvite">Decline</button>
|
||||
<button class="btn btn-primary font-weight-bold btn-sm" @click="acceptInvite">Accept</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <p class="text-center h4 font-weight-bold">by <a href="#" class="font-weight-bold">@dansup</a></p> -->
|
||||
</div>
|
||||
|
||||
<div v-else-if="tab === 'existingmember'" key="existingmember">
|
||||
<p class="text-center mb-0">You already are a member of this group</p>
|
||||
<p class="text-center mb-0">
|
||||
<a :href="group.url" class="font-weight-bold">View Group</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-else-if="tab === 'notinvited'" key="notinvited">
|
||||
<p class="text-center mb-0">We cannot find an active invitation for your account.</p>
|
||||
</div>
|
||||
|
||||
<div v-else-if="tab === 'error'" key="error">
|
||||
<p class="text-center mb-0">An unknown error occured. Please try again later.</p>
|
||||
</div>
|
||||
|
||||
<div v-else key="unknown">
|
||||
<p class="text-center mb-0">An unknown error occured. Please try again later.</p>
|
||||
</div>
|
||||
|
||||
</transition-group>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <h4>You've been invited to {{ group.name }}</h4> -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script type="text/javascript">
|
||||
export default {
|
||||
props: [
|
||||
'id'
|
||||
],
|
||||
|
||||
data() {
|
||||
return {
|
||||
loadingStatus: 'Determining invite eligibility',
|
||||
tab: 'initial',
|
||||
profile: {},
|
||||
group: {},
|
||||
showMore: false
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
axios.get('/api/pixelfed/v1/accounts/verify_credentials')
|
||||
.then(res => {
|
||||
this.profile = res.data;
|
||||
this.fetchGroup();
|
||||
}).catch(err => {
|
||||
if(err.response.status === 403) {
|
||||
this.tab = 'login';
|
||||
return;
|
||||
} else {
|
||||
this.tab = 'error';
|
||||
return
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
methods: {
|
||||
fetchGroup() {
|
||||
axios.get(`/api/v0/groups/${this.id}`)
|
||||
.then(res => {
|
||||
this.group = res.data;
|
||||
this.loadingStatus = 'Checking group invitations';
|
||||
this.checkForInvitation();
|
||||
}).catch(err => {
|
||||
this.tab = 'error';
|
||||
});
|
||||
},
|
||||
|
||||
checkForInvitation() {
|
||||
axios.post(`/api/v0/groups/${this.group.id}/invite/check`)
|
||||
.then(res => {
|
||||
this.tab = res.data.can_join == true ? 'form' : 'notinvited';
|
||||
}).catch(err => {
|
||||
if(err.response.status === 422 && err.response.data.error === 'Already a member') {
|
||||
this.tab = 'existingmember';
|
||||
} else {
|
||||
this.tab = 'error';
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
prettyCount(val) {
|
||||
return App.util.format.count(val);
|
||||
},
|
||||
|
||||
timeago(ts) {
|
||||
return App.util.format.timeAgo(ts);
|
||||
},
|
||||
|
||||
showMoreInfo() {
|
||||
event.currentTarget.blur();
|
||||
this.showMore = !this.showMore;
|
||||
},
|
||||
|
||||
acceptInvite() {
|
||||
event.currentTarget.blur();
|
||||
this.tab = 'loading';
|
||||
axios.post(`/api/v0/groups/${this.group.id}/invite/accept`)
|
||||
.then(res => {
|
||||
setTimeout(() => {
|
||||
location.href = res.data.next_url;
|
||||
}, 2000);
|
||||
}).catch(err => {
|
||||
this.tab = 'error';
|
||||
});
|
||||
},
|
||||
|
||||
declineInvite() {
|
||||
event.currentTarget.blur();
|
||||
this.tab = 'loading';
|
||||
axios.post(`/api/v0/groups/${this.group.id}/invite/decline`)
|
||||
.then(res => {
|
||||
location.href = res.data.next_url;
|
||||
}).catch(err => {
|
||||
this.tab = 'error';
|
||||
});
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.group-invite-component {
|
||||
.btn-light {
|
||||
border-color: #E5E7EB;
|
||||
}
|
||||
}
|
||||
</style>
|
379
resources/assets/components/groups/GroupProfile.vue
Normal file
379
resources/assets/components/groups/GroupProfile.vue
Normal file
|
@ -0,0 +1,379 @@
|
|||
<template>
|
||||
<div class="group-profile-component w-100 h-100">
|
||||
<div class="bg-white mb-3 border-bottom">
|
||||
<div class="container-xl header">
|
||||
<div class="header-jumbotron"></div>
|
||||
|
||||
<div class="header-profile-card">
|
||||
<img :src="profile.avatar" class="avatar" onerror="this.onerror=null;this.src='/storage/avatars/default.png?v=2'">
|
||||
<p class="name">
|
||||
{{ profile.display_name }}
|
||||
</p>
|
||||
<p class="username text-muted">
|
||||
<span v-if="profile.local">@{{ profile.username }}</span>
|
||||
<span v-else>{{ profile.acct }}</span>
|
||||
|
||||
<span v-if="profile.is_admin" class="text-danger ml-1" title="Site administrator" data-toggle="tooltip" data-placement="bottom"><i class="far fa-users-crown"></i></span>
|
||||
</p>
|
||||
</div>
|
||||
<!-- <hr> -->
|
||||
<div class="header-navbar">
|
||||
<div></div>
|
||||
|
||||
<div>
|
||||
<a
|
||||
v-if="currentProfile.id === profile.id"
|
||||
class="btn btn-light font-weight-bold mr-2"
|
||||
href="/settings/home">
|
||||
<i class="fas fa-edit mr-1"></i> Edit Profile
|
||||
</a>
|
||||
|
||||
<a
|
||||
v-if="profile.relationship.following"
|
||||
class="btn btn-primary font-weight-bold mr-2"
|
||||
:href="profile.url">
|
||||
<i class="far fa-comment-alt-dots mr-1"></i> Message
|
||||
</a>
|
||||
|
||||
<a
|
||||
v-if="profile.relationship.following"
|
||||
class="btn btn-light font-weight-bold mr-2"
|
||||
:href="profile.url">
|
||||
<i class="fas fa-user-check mr-1"></i> {{ profile.relationship.followed_by ? 'Friends' : 'Following' }}
|
||||
</a>
|
||||
|
||||
<a
|
||||
v-if="!profile.relationship.following"
|
||||
class="btn btn-light font-weight-bold mr-2"
|
||||
:href="profile.url">
|
||||
<i class="fas fa-user mr-1"></i> View Main Profile
|
||||
</a>
|
||||
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-light font-weight-bold dropdown-toggle" type="button" id="amenu" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<i class="fas fa-ellipsis-h"></i>
|
||||
</button>
|
||||
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="amenu">
|
||||
<a v-if="currentProfile.id != profile.id" class="dropdown-item font-weight-bold" :href="`/i/report?type=user&id=${profile.id}`">Report</a>
|
||||
<a v-if="currentProfile.id == profile.id" class="dropdown-item font-weight-bold" href="#">Leave Group</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-100 h-100 group-profile-feed">
|
||||
<div class="container-xl">
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-5">
|
||||
<div class="card card-body shadow-sm infolet">
|
||||
<h5 class="font-weight-bold mb-3">Intro</h5>
|
||||
<div v-if="!profile.local" class="media mb-3 align-items-center">
|
||||
<div class="media-icon">
|
||||
<i class="far fa-globe" title="User is from a remote server" data-toggle="tooltip" data-placement="bottom"></i>
|
||||
</div>
|
||||
<div class="media-body">
|
||||
Remote member from <strong>{{ profile.acct.split('@')[1] }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <div v-if="profile.note.length" class="media mb-3 align-items-center">
|
||||
<i class="far fa-book-user fa-lg text-lighter mr-3" title="User bio" data-toggle="tooltip" data-placement="bottom"></i>
|
||||
<div class="media-body">
|
||||
<span v-html="profile.note"></span>
|
||||
</div>
|
||||
</div> -->
|
||||
<div class="media align-items-center">
|
||||
<div class="media-icon">
|
||||
<i class="fas fa-users" title="User joined group on this date" data-toggle="tooltip" data-placement="bottom"></i>
|
||||
</div>
|
||||
<div class="media-body">
|
||||
{{ roleTitle }} of <strong>{{ group.name }}</strong> since {{ profile.group.joined }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="canIntersect" class="card card-body shadow-sm infolet">
|
||||
<h5 class="font-weight-bold mb-3">Things in Common</h5>
|
||||
|
||||
<div v-if="commonIntersects.friends.length" class="media mb-3 align-items-center" v-once>
|
||||
<div class="media-icon">
|
||||
<i class="far fa-user-friends"></i>
|
||||
</div>
|
||||
<div class="media-body">
|
||||
{{ commonIntersects.friends_count }} mutual friend<span v-if="commonIntersects.friends.length > 1">s</span> including
|
||||
<span v-for="(friend, index) in commonIntersects.friends"><a :href="friend.url" class="text-dark font-weight-bold">{{ friend.acct }}</a><span v-if="commonIntersects.friends.length > index + 1">, </span><span v-else> </span></span>
|
||||
<!-- <a href="#" class="text-dark font-weight-bold">dansup</a>, <a href="#" class="text-dark font-weight-bold">admin</a> and 1 other -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="media mb-3 align-items-center">
|
||||
<div class="media-icon">
|
||||
<i class="fas fa-home"></i>
|
||||
</div>
|
||||
<div class="media-body">
|
||||
Lives in <strong>Canada</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="commonIntersects.groups.length" class="media mb-3 align-items-center">
|
||||
<div class="media-icon">
|
||||
<i class="fas fa-users"></i>
|
||||
</div>
|
||||
<div class="media-body">
|
||||
Also member of <a :href="commonIntersects.groups[0].url" class="text-dark font-weight-bold">{{ commonIntersects.groups[0].name }}</a> and {{ commonIntersects.groups_count }} other groups
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="commonIntersects.topics.length" class="media mb-0 align-items-center">
|
||||
<div class="media-icon">
|
||||
<i class="far fa-thumbs-up fa-lg text-lighter"></i>
|
||||
</div>
|
||||
<div class="media-body">
|
||||
Also interested in topics containing
|
||||
<span v-for="(topic, index) in commonIntersects.topics">
|
||||
<span v-if="commonIntersects.topics.length - 1 == index"> and </span><a :href="topic.url" class="font-weight-bold text-dark">#{{ topic.name }}</a><span v-if="commonIntersects.topics.length > index + 2">, </span>
|
||||
</span> hashtags
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-7">
|
||||
<div class="card card-body shadow-sm">
|
||||
<h5 class="font-weight-bold mb-0">Group Posts</h5>
|
||||
</div>
|
||||
|
||||
<div v-if="feedEmpty" class="pt-5 text-center">
|
||||
<h5>No New Posts</h5>
|
||||
<p>{{ profile.username }} hasn't posted anything yet in <strong>{{ group.name }}</strong>.</p>
|
||||
|
||||
<a :href="group.url" class="font-weight-bold">Go Back</a>
|
||||
</div>
|
||||
|
||||
<div v-if="feedLoaded" class="mt-2">
|
||||
<group-status
|
||||
v-for="(status, index) in feed"
|
||||
:key="'gps:' + status.id"
|
||||
:permalinkMode="true"
|
||||
:showGroupChevron="true"
|
||||
:group="group"
|
||||
:prestatus="status"
|
||||
:profile="profile"
|
||||
:group-id="group.id" />
|
||||
|
||||
<div v-if="feed.length >= 1" :distance="800">
|
||||
<infinite-loading @infinite="infiniteFeed">
|
||||
<div slot="no-more"></div>
|
||||
<div slot="no-results"></div>
|
||||
</infinite-loading>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script type="text/javascript">
|
||||
import GroupStatus from '@/groups/partials/GroupStatus.vue';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
pg: {
|
||||
type: String
|
||||
},
|
||||
|
||||
pp: {
|
||||
type: String
|
||||
}
|
||||
},
|
||||
|
||||
components: {
|
||||
'group-status': GroupStatus
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
currentProfile: {},
|
||||
roleTitle: 'Member',
|
||||
group: {},
|
||||
profile: {},
|
||||
feed: [],
|
||||
ids: [],
|
||||
feedLoaded: false,
|
||||
feedEmpty: false,
|
||||
page: 1,
|
||||
canIntersect: false,
|
||||
commonIntersects: []
|
||||
}
|
||||
},
|
||||
|
||||
beforeMount() {
|
||||
$('body').css('background-color', '#f0f2f5');
|
||||
this.group = JSON.parse(this.pg);
|
||||
this.profile = JSON.parse(this.pp);
|
||||
|
||||
if(this.profile.group.role == 'founder') {
|
||||
this.roleTitle = 'Admin';
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
axios.get('/api/pixelfed/v1/accounts/verify_credentials')
|
||||
.then(res => {
|
||||
this.currentProfile = res.data;
|
||||
this.fetchInitialFeed();
|
||||
if(res.data.id != this.profile.id) {
|
||||
this.fetchCommonIntersections();
|
||||
}
|
||||
})
|
||||
this.$nextTick(() => {
|
||||
$('[data-toggle="tooltip"]').tooltip();
|
||||
});
|
||||
},
|
||||
|
||||
methods: {
|
||||
fetchInitialFeed() {
|
||||
axios.get(`/api/v0/groups/${this.group.id}/user/${this.profile.id}/feed`)
|
||||
.then(res => {
|
||||
this.feed = res.data.filter(s => {
|
||||
return s.pf_type != 'reply:text' || s.account.id != this.profile.id;
|
||||
});
|
||||
this.feedLoaded = true;
|
||||
this.feedEmpty = this.feed.length == 0;
|
||||
this.page++;
|
||||
});
|
||||
},
|
||||
|
||||
infiniteFeed($state) {
|
||||
if(this.feed.length == 0) {
|
||||
$state.complete();
|
||||
return;
|
||||
}
|
||||
|
||||
axios.get(`/api/v0/groups/${this.group.id}/user/${this.profile.id}/feed`, {
|
||||
params: {
|
||||
page: this.page
|
||||
},
|
||||
}).then(res => {
|
||||
if (res.data.length) {
|
||||
let data = res.data.filter(s => {
|
||||
return s.pf_type != 'reply:text' || s.account.id != this.profile.id;
|
||||
});
|
||||
let self = this;
|
||||
data.forEach(d => {
|
||||
if(self.ids.indexOf(d.id) == -1) {
|
||||
self.ids.push(d.id);
|
||||
self.feed.push(d);
|
||||
}
|
||||
});
|
||||
$state.loaded();
|
||||
this.page++;
|
||||
} else {
|
||||
$state.complete();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
fetchCommonIntersections() {
|
||||
axios.get('/api/v0/groups/member/intersect/common', {
|
||||
params: {
|
||||
gid: this.group.id,
|
||||
pid: this.profile.id
|
||||
}
|
||||
}).then(res => {
|
||||
this.commonIntersects = res.data;
|
||||
this.canIntersect = res.data.groups.length || res.data.topics.length;
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.group-profile-component {
|
||||
background-color: #f0f2f5;
|
||||
|
||||
.header {
|
||||
&-jumbotron {
|
||||
background-color: #F3F4F6;
|
||||
height: 320px;
|
||||
border-bottom-left-radius: 20px;
|
||||
border-bottom-right-radius: 20px;
|
||||
}
|
||||
|
||||
&-profile-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
.avatar {
|
||||
width: 170px;
|
||||
height: 170px;
|
||||
border-radius: 50%;
|
||||
margin-top: -150px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-size: 30px;
|
||||
line-height: 30px;
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.username {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
&-navbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
height: 60px;
|
||||
border-top: 1px solid #F3F4F6;
|
||||
|
||||
.dropdown {
|
||||
display: inline-block;
|
||||
|
||||
&-toggle:after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.group-profile-feed {
|
||||
min-height: 500px;
|
||||
}
|
||||
|
||||
.infolet {
|
||||
margin-bottom: 1rem;
|
||||
|
||||
.media {
|
||||
&-icon {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 30px;
|
||||
margin-right: 10px;
|
||||
|
||||
i {
|
||||
font-size: 1.1rem;
|
||||
color: #D1D5DB !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.btn-light {
|
||||
border-color: #F3F4F6;
|
||||
}
|
||||
}
|
||||
</style>
|
1079
resources/assets/components/groups/GroupSettings.vue
Normal file
1079
resources/assets/components/groups/GroupSettings.vue
Normal file
File diff suppressed because it is too large
Load diff
170
resources/assets/components/groups/GroupTopicFeed.vue
Normal file
170
resources/assets/components/groups/GroupTopicFeed.vue
Normal file
|
@ -0,0 +1,170 @@
|
|||
<template>
|
||||
<div class="group-topic-feed-component">
|
||||
<div v-if="isLoaded" class="bg-white py-5 border-bottom">
|
||||
<div class="container">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<h3 class="font-weight-bold mb-1">#{{ name }}</h3>
|
||||
<p class="mb-0 lead text-muted">
|
||||
<span>
|
||||
Posts in <a :href="group.url" class="text-muted font-weight-bold">{{ group.name }}</a>
|
||||
</span>
|
||||
<span>·</span>
|
||||
<span><i class="fas fa-globe"></i></span>
|
||||
<span>·</span>
|
||||
<span>{{ group.membership != 'all' ? 'Private' : 'Public'}} Group</span>
|
||||
</p>
|
||||
</div>
|
||||
<!-- <div>
|
||||
<button class="btn btn-light border btn-lg text-muted">
|
||||
<i class="fas fa-ellipsis-h"></i>
|
||||
</button>
|
||||
</div> -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="isLoaded" class="row justify-content-center mt-3">
|
||||
<div v-if="feed.length" class="col-12 col-md-5">
|
||||
<group-status
|
||||
v-for="(status, index) in feed"
|
||||
:key="'gs:' + status.id + index"
|
||||
:prestatus="status"
|
||||
:profile="profile"
|
||||
:group="group"
|
||||
:show-group-chevron="true"
|
||||
:group-id="gid" />
|
||||
|
||||
<div v-if="feed.length > 2">
|
||||
<infinite-loading @infinite="infiniteFeed">
|
||||
<div slot="no-more"></div>
|
||||
<div slot="no-results"></div>
|
||||
</infinite-loading>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="col-12 col-md-5 d-flex justify-content-center">
|
||||
<div class="mt-5">
|
||||
<p class="text-lighter text-center">
|
||||
<i class="fal fa-exclamation-circle fa-4x"></i>
|
||||
</p>
|
||||
|
||||
<p class="font-weight-bold text-muted">Cannot load any posts containg the <span class="font-weight-normal">#{{ name }}</span> hashtag</p>
|
||||
|
||||
<p class="text-left">
|
||||
This can happen for a few reasons:
|
||||
</p>
|
||||
|
||||
<ul class="text-left">
|
||||
<li>There is a typo in the url</li>
|
||||
<li>No posts exist that contain this hashtag</li>
|
||||
<li>This hashtag has been banned by group admins</li>
|
||||
<li>The hashtag is new or used infrequently</li>
|
||||
<li>A technical issue has occured</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script type="text/javascript">
|
||||
import GroupStatus from '@/groups/partials/GroupStatus.vue';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
gid: {
|
||||
type: String
|
||||
},
|
||||
|
||||
name: {
|
||||
type: String
|
||||
}
|
||||
},
|
||||
|
||||
components: {
|
||||
GroupStatus
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
isLoaded: false,
|
||||
group: false,
|
||||
profile: false,
|
||||
feed: [],
|
||||
page: 1,
|
||||
ids: []
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.fetchProfile();
|
||||
},
|
||||
|
||||
methods: {
|
||||
fetchProfile() {
|
||||
axios.get('/api/pixelfed/v1/accounts/verify_credentials')
|
||||
.then(res => {
|
||||
this.profile = res.data;
|
||||
this.fetchGroup();
|
||||
});
|
||||
},
|
||||
|
||||
fetchGroup() {
|
||||
axios.get('/api/v0/groups/' + this.gid)
|
||||
.then(res => {
|
||||
this.group = res.data;
|
||||
this.fetchFeed();
|
||||
});
|
||||
},
|
||||
|
||||
fetchFeed() {
|
||||
axios.get('/api/v0/groups/topics/tag', {
|
||||
params: {
|
||||
gid: this.gid,
|
||||
name: this.name
|
||||
}
|
||||
}).then(res => {
|
||||
this.feed = res.data;
|
||||
this.isLoaded = true;
|
||||
let self = this;
|
||||
res.data.forEach(d => {
|
||||
if(self.ids.indexOf(d.id) == -1) {
|
||||
self.ids.push(d.id);
|
||||
}
|
||||
});
|
||||
this.page++;
|
||||
})
|
||||
},
|
||||
|
||||
infiniteFeed($state) {
|
||||
if(this.feed.length < 2) {
|
||||
$state.complete();
|
||||
return;
|
||||
}
|
||||
|
||||
axios.get('/api/v0/groups/topics/tag', {
|
||||
params: {
|
||||
gid: this.gid,
|
||||
name: this.name,
|
||||
limit: 1,
|
||||
page: this.page
|
||||
},
|
||||
}).then(res => {
|
||||
if (res.data.length) {
|
||||
let data = res.data;
|
||||
let self = this;
|
||||
data.forEach(d => {
|
||||
if(self.ids.indexOf(d.id) == -1) {
|
||||
self.ids.push(d.id);
|
||||
self.feed.push(d);
|
||||
}
|
||||
});
|
||||
$state.loaded();
|
||||
this.page++;
|
||||
} else {
|
||||
$state.complete();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
473
resources/assets/components/groups/GroupsHome.vue
Normal file
473
resources/assets/components/groups/GroupsHome.vue
Normal file
|
@ -0,0 +1,473 @@
|
|||
<template>
|
||||
<div class="groups-home-component w-100 h-100">
|
||||
<div v-if="initialLoad" class="row border-bottom m-0 p-0">
|
||||
<div class="col-2 shadow" style="height: 100vh;background:#fff;top:51px;overflow: hidden;z-index: 1;position: sticky;">
|
||||
<div class="p-1">
|
||||
<div class="d-flex justify-content-between align-items-center py-3">
|
||||
<p class="h2 font-weight-bold mb-0">Groups</p>
|
||||
<a class="btn btn-light px-2 rounded-circle" href="/settings/home">
|
||||
<i class="fas fa-cog fa-lg"></i>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<autocomplete
|
||||
:search="autocompleteSearch"
|
||||
placeholder="Search groups by name"
|
||||
aria-label="Search groups by name"
|
||||
:get-result-value="getSearchResultValue"
|
||||
:debounceTime="700"
|
||||
@submit="onSearchSubmit"
|
||||
ref="autocomplete"
|
||||
>
|
||||
<template #result="{ result, props }">
|
||||
<li
|
||||
v-bind="props"
|
||||
class="autocomplete-result"
|
||||
>
|
||||
|
||||
<div class="media align-items-center">
|
||||
<img v-if="result.local && result.metadata && result.metadata.hasOwnProperty('header') && result.metadata.header.hasOwnProperty('url')" :src="result.metadata.header.url" width="32" height="32">
|
||||
<div v-else class="icon-placeholder">
|
||||
<i class="fal fa-user-friends"></i>
|
||||
</div>
|
||||
<div class="media-body text-truncate mr-3">
|
||||
<p class="result-name mb-n1 font-weight-bold">
|
||||
{{ truncateName(result.name) }}
|
||||
<span v-if="result.verified" class="fa-stack ml-n2" title="Verified Group" data-toggle="tooltip">
|
||||
<i class="fas fa-circle fa-stack-1x fa-lg" style="color: #22a7f0cc;font-size:18px"></i>
|
||||
<i class="fas fa-check fa-stack-1x text-white" style="font-size:10px"></i>
|
||||
</span>
|
||||
</p>
|
||||
<p class="mb-0 text-muted" style="font-size: 10px;">
|
||||
<span v-if="!result.local" title="Remote Group">
|
||||
<i class="far fa-globe"></i>
|
||||
</span>
|
||||
<span v-if="!result.local">·</span>
|
||||
<span class="font-weight-bold">{{ result.member_count }} members</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</template>
|
||||
</autocomplete>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="btn btn-light group-nav-btn"
|
||||
:class="{ active: tab == 'feed' }"
|
||||
@click="switchTab('feed')">
|
||||
<div class="group-nav-btn-icon">
|
||||
<i class="fas fa-list"></i>
|
||||
</div>
|
||||
<div class="group-nav-btn-name">
|
||||
Your Feed
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="btn btn-light group-nav-btn"
|
||||
:class="{ active: tab == 'discover' }"
|
||||
@click="switchTab('discover')">
|
||||
<div class="group-nav-btn-icon">
|
||||
<i class="fas fa-compass"></i>
|
||||
</div>
|
||||
<div class="group-nav-btn-name">
|
||||
Discover
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="btn btn-light group-nav-btn"
|
||||
:class="{ active: tab == 'mygroups' }"
|
||||
@click="switchTab('mygroups')">
|
||||
<div class="group-nav-btn-icon">
|
||||
<i class="fas fa-list"></i>
|
||||
</div>
|
||||
<div class="group-nav-btn-name">
|
||||
My Groups
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="btn btn-light group-nav-btn"
|
||||
:class="{ active: tab == 'notifications' }"
|
||||
@click="switchTab('notifications')">
|
||||
<div class="group-nav-btn-icon">
|
||||
<i class="far fa-bell"></i>
|
||||
</div>
|
||||
<div class="group-nav-btn-name">
|
||||
Your Notifications
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<!-- <button
|
||||
class="btn btn-light group-nav-btn"
|
||||
:class="{ active: tab == 'invitations' }"
|
||||
@click="switchTab('invitations')">
|
||||
<div class="group-nav-btn-icon">
|
||||
<i class="fas fa-user-plus"></i>
|
||||
</div>
|
||||
<div class="group-nav-btn-name">
|
||||
Group Invitations
|
||||
</div>
|
||||
</button> -->
|
||||
|
||||
<button
|
||||
class="btn btn-light group-nav-btn"
|
||||
:class="{ active: tab == 'remotesearch' }"
|
||||
@click="switchTab('remotesearch')">
|
||||
<div class="group-nav-btn-icon">
|
||||
<i class="fas fa-search-plus"></i>
|
||||
</div>
|
||||
<div class="group-nav-btn-name">
|
||||
Find a remote group
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
v-if="config && config.limits.user.create.new"
|
||||
class="btn btn-primary btn-block rounded-pill font-weight-bold mt-3"
|
||||
@click="switchTab('creategroup')"
|
||||
:disabled="tab == 'creategroup'">
|
||||
<i class="fas fa-plus mr-2"></i> Create New Group
|
||||
</button>
|
||||
|
||||
<hr>
|
||||
<div v-for="group in groups" class="ml-2">
|
||||
<div class="card shadow-sm border text-decoration-none text-dark">
|
||||
<img v-if="group.metadata && group.metadata.hasOwnProperty('header')" :src="group.metadata.header.url" class="card-img-top" style="width: 100%; height: auto;object-fit: cover;max-height: 160px;">
|
||||
<div v-else class="bg-primary" style="width:100%;height:160px;"></div>
|
||||
<div class="card-body">
|
||||
<div class="lead font-weight-bold d-flex align-items-top" style="height: 60px;">
|
||||
{{ group.name }}
|
||||
<span v-if="group.verified" class="fa-stack ml-n2 mt-n2">
|
||||
<i class="fas fa-circle fa-stack-1x fa-lg" style="color: #22a7f0cc;font-size:18px"></i>
|
||||
<i class="fas fa-check fa-stack-1x text-white" style="font-size:10px"></i>
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-muted font-weight-light d-flex justify-content-between">
|
||||
<span>{{group.member_count}} Members</span>
|
||||
<span style="font-size: 12px;padding: 2px 5px;color: rgba(75, 119, 190, 1);background:rgba(137, 196, 244, 0.2);border:1px solid rgba(137, 196, 244, 0.3);font-weight:400;text-transform: capitalize;" class="rounded">{{ group.self.role }}</span>
|
||||
</div>
|
||||
<hr>
|
||||
<p class="mb-0">
|
||||
<a class="btn btn-light btn-block border rounded-lg font-weight-bold" :href="group.url">View Group</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<keep-alive>
|
||||
<transition name="fade">
|
||||
<self-feed v-if="tab == 'feed'" :profile="profile" v-on:switchtab="switchTab" />
|
||||
<self-discover v-if="tab == 'discover'" :profile="profile" />
|
||||
<self-notifications v-if="tab == 'notifications'" :profile="profile" />
|
||||
<self-invitations v-if="tab == 'invitations'" :profile="profile" />
|
||||
<self-remote-search v-if="tab == 'remotesearch'" :profile="profile" />
|
||||
<self-groups v-if="tab == 'mygroups'" :profile="profile" />
|
||||
<create-group v-if="tab == 'creategroup'" :profile="profile" />
|
||||
<div v-if="tab == 'gsearch'">
|
||||
<div class="col-12 px-5">
|
||||
<div class="my-4">
|
||||
<p class="h1 font-weight-bold mb-1">Group Search</p>
|
||||
<p class="lead text-muted mb-0">Search and explore groups.</p>
|
||||
</div>
|
||||
<div class="media align-items-center text-lighter">
|
||||
<i class="far fa-chevron-left fa-lg mr-3"></i>
|
||||
<div class="media-body">
|
||||
<p class="lead mb-0">Use the search bar on the side menu</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</keep-alive>
|
||||
</div>
|
||||
<div v-else class="row justify-content-center mt-5">
|
||||
<b-spinner />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script type="text/javascript">
|
||||
import GroupStatus from './partials/GroupStatus.vue';
|
||||
import SelfFeed from './partials/SelfFeed.vue';
|
||||
import SelfDiscover from './partials/SelfDiscover.vue';
|
||||
import SelfGroups from './partials/SelfGroups.vue';
|
||||
import SelfNotifications from './partials/SelfNotifications.vue';
|
||||
import SelfInvitations from './partials/SelfInvitations.vue';
|
||||
import SelfRemoteSearch from './partials/SelfRemoteSearch.vue';
|
||||
import CreateGroup from './CreateGroup.vue';
|
||||
import Autocomplete from '@trevoreyre/autocomplete-vue'
|
||||
import '@trevoreyre/autocomplete-vue/dist/style.css'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
initialLoad: false,
|
||||
config: {},
|
||||
groups: [],
|
||||
profile: {},
|
||||
tab: null,
|
||||
searchQuery: undefined,
|
||||
};
|
||||
},
|
||||
|
||||
components: {
|
||||
'autocomplete-input': Autocomplete,
|
||||
'group-status': GroupStatus,
|
||||
'self-discover': SelfDiscover,
|
||||
'self-groups': SelfGroups,
|
||||
'self-feed': SelfFeed,
|
||||
'self-notifications': SelfNotifications,
|
||||
'self-invitations': SelfInvitations,
|
||||
'self-remote-search': SelfRemoteSearch,
|
||||
"create-group": CreateGroup
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.fetchConfig();
|
||||
},
|
||||
|
||||
methods: {
|
||||
init() {
|
||||
document.querySelectorAll("footer").forEach(e => e.parentNode.removeChild(e));
|
||||
document.querySelectorAll(".mobile-footer-spacer").forEach(e => e.parentNode.removeChild(e));
|
||||
document.querySelectorAll(".mobile-footer").forEach(e => e.parentNode.removeChild(e));
|
||||
// let u = new URLSearchParams(window.location.search);
|
||||
// if(u.has('ct')) {
|
||||
// if(['mygroups', 'notifications', 'discover', 'remotesearch', 'creategroup', 'gsearch'].includes(u.get('ct'))) {
|
||||
// if(u.get('ct') == 'creategroup' && this.config.limits.user.create.new == false) {
|
||||
// this.tab = 'feed';
|
||||
// history.pushState(null, null, '/groups/feed');
|
||||
// } else {
|
||||
// this.tab = u.get('ct');
|
||||
// }
|
||||
// } else {
|
||||
// this.tab = 'feed';
|
||||
// history.pushState(null, null, '/groups/feed');
|
||||
// }
|
||||
// } else {
|
||||
// this.tab = 'feed';
|
||||
// }
|
||||
this.initialLoad = true;
|
||||
},
|
||||
|
||||
fetchConfig() {
|
||||
axios.get('/api/v0/groups/config')
|
||||
.then(res => {
|
||||
this.config = res.data;
|
||||
this.fetchProfile();
|
||||
});
|
||||
},
|
||||
|
||||
fetchProfile() {
|
||||
axios.get('/api/pixelfed/v1/accounts/verify_credentials')
|
||||
.then(res => {
|
||||
this.profile = res.data;
|
||||
this.init();
|
||||
window._sharedData.curUser = res.data;
|
||||
window.App.util.navatar();
|
||||
})
|
||||
},
|
||||
|
||||
fetchSelfGroups() {
|
||||
axios.get('/api/v0/groups/self/list')
|
||||
.then(res => {
|
||||
this.groups = res.data;
|
||||
});
|
||||
},
|
||||
|
||||
switchTab(tab) {
|
||||
event.currentTarget.blur();
|
||||
window.scrollTo(0,0);
|
||||
this.tab = tab;
|
||||
|
||||
if(tab != 'feed') {
|
||||
history.pushState(null, null, '/groups/home?ct=' + tab);
|
||||
} else {
|
||||
history.pushState(null, null, '/groups/home');
|
||||
}
|
||||
},
|
||||
|
||||
autocompleteSearch(input) {
|
||||
if (!input || input.length < 2) {
|
||||
if(this.tab = 'searchresults') {
|
||||
this.tab = 'feed';
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
this.searchQuery = input;
|
||||
// this.tab = 'searchresults';
|
||||
|
||||
if(input.startsWith('http')) {
|
||||
let url = new URL(input);
|
||||
if(url.hostname == location.hostname) {
|
||||
location.href = input;
|
||||
return [];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
if(input.startsWith('#')) {
|
||||
this.$bvToast.toast(input, {
|
||||
title: 'Hashtag detected',
|
||||
variant: 'info',
|
||||
autoHideDelay: 5000
|
||||
});
|
||||
return [];
|
||||
}
|
||||
|
||||
return axios.post('/api/v0/groups/search/global', {
|
||||
q: input,
|
||||
v: '0.2'
|
||||
})
|
||||
.then(res => {
|
||||
this.searchLoading = false;
|
||||
return res.data;
|
||||
}).catch(err => {
|
||||
|
||||
if(err.response.status === 422) {
|
||||
this.$bvToast.toast(err.response.data.error.message, {
|
||||
title: 'Cannot display search results',
|
||||
variant: 'danger',
|
||||
autoHideDelay: 5000
|
||||
});
|
||||
}
|
||||
|
||||
return [];
|
||||
})
|
||||
},
|
||||
|
||||
getSearchResultValue(result) {
|
||||
return result.name;
|
||||
},
|
||||
|
||||
onSearchSubmit(result) {
|
||||
if (result.length < 1) {
|
||||
return [];
|
||||
}
|
||||
|
||||
location.href = result.url;
|
||||
},
|
||||
|
||||
truncateName(val) {
|
||||
if(val.length < 24) {
|
||||
return val;
|
||||
}
|
||||
|
||||
return val.substr(0, 23) + '...';
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.groups-home-component {
|
||||
font-family: var(--font-family-sans-serif);
|
||||
|
||||
.group-nav-btn {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding-left: 0;
|
||||
padding-top: 0.3rem;
|
||||
padding-bottom: 0.3rem;
|
||||
margin-bottom: 0.3rem;
|
||||
border-radius: 1.5rem;
|
||||
text-align: left;
|
||||
color: #6c757d;
|
||||
background-color: transparent;
|
||||
border-color: transparent;
|
||||
justify-content: flex-start;
|
||||
|
||||
&.active {
|
||||
background-color: #EFF6FF !important;
|
||||
border:1px solid #DBEAFE !important;
|
||||
color: #212529;
|
||||
|
||||
.group-nav-btn-icon {
|
||||
background-color: #2c78bf !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
}
|
||||
|
||||
&-icon {
|
||||
display: inline-flex;
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
padding: 12px;
|
||||
background-color: #E5E7EB;
|
||||
border-radius: 17px;
|
||||
margin: auto 0.3rem;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
&-name {
|
||||
display: inline-block;
|
||||
margin-left: 0.3rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
.autocomplete-input {
|
||||
height: 2.375rem;
|
||||
background-color: #f8f9fa !important;
|
||||
font-size: 0.9rem;
|
||||
color: #495057;
|
||||
border-radius: 50rem;
|
||||
border-color: transparent;
|
||||
|
||||
&:focus,
|
||||
&[aria-expanded=true] {
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
.autocomplete-result {
|
||||
background: none;
|
||||
padding: 12px;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background-color: #EFF6FF !important;
|
||||
}
|
||||
|
||||
.media {
|
||||
img {
|
||||
object-fit: cover;
|
||||
border-radius: 4px;
|
||||
margin-right: 0.6rem;
|
||||
}
|
||||
|
||||
.icon-placeholder {
|
||||
display: flex;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background-color: #2c78bf;
|
||||
border-radius: 4px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: #fff;
|
||||
margin-right: 0.6rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.autocomplete-result-list {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.fade-enter-active, .fade-leave-active {
|
||||
transition: opacity 200ms;
|
||||
}
|
||||
|
||||
.fade-enter, .fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
168
resources/assets/components/groups/Page/GroupAbout.vue
Normal file
168
resources/assets/components/groups/Page/GroupAbout.vue
Normal file
|
@ -0,0 +1,168 @@
|
|||
<template>
|
||||
<div class="group-feed-component">
|
||||
<div class="row border-bottom m-0 p-0">
|
||||
<sidebar />
|
||||
|
||||
<div class="col-12 col-md-9 px-md-0">
|
||||
<div class="bg-white mb-3 border-bottom">
|
||||
<div>
|
||||
<group-banner :group="group" />
|
||||
<group-header-details
|
||||
:group="group"
|
||||
:isAdmin="isAdmin"
|
||||
:isMember="isMember"
|
||||
@refresh="handleRefresh"
|
||||
/>
|
||||
<group-nav-tabs
|
||||
:group="group"
|
||||
:isAdmin="isAdmin"
|
||||
:isMember="isMember"
|
||||
:atabs="atabs"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="!initialLoad">
|
||||
<p class="text-center mt-5 pt-5 font-weight-bold">Loading...</p>
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<div class="container-xl group-feed-component-body">
|
||||
<template v-if="initialLoad && group.self.is_member">
|
||||
<group-about :key="renderIdx" :group="group" :profile="profile" />
|
||||
</template>
|
||||
|
||||
<member-only-warning v-else />
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script type="text/javascript">
|
||||
import StatusCard from '~/partials/StatusCard.vue';
|
||||
import GroupMembers from '@/groups/partials/GroupMembers.vue';
|
||||
import GroupCompose from '@/groups/partials/GroupCompose.vue';
|
||||
import GroupStatus from '@/groups/partials/GroupStatus.vue';
|
||||
import GroupAbout from '@/groups/partials/GroupAbout.vue';
|
||||
import GroupMedia from '@/groups/partials/GroupMedia.vue';
|
||||
import GroupModeration from '@/groups/partials/GroupModeration.vue';
|
||||
import GroupTopics from '@/groups/partials/GroupTopics.vue';
|
||||
import GroupInfoCard from '@/groups/partials/GroupInfoCard.vue';
|
||||
import LeaveGroup from '@/groups/partials/LeaveGroup.vue';
|
||||
import GroupInsights from '@/groups/partials/GroupInsights.vue';
|
||||
import SearchModal from '@/groups/partials/GroupSearchModal.vue';
|
||||
import InviteModal from '@/groups/partials/GroupInviteModal.vue';
|
||||
import SidebarComponent from '@/groups/sections/Sidebar.vue';
|
||||
import GroupBanner from '@/groups/partials/Page/GroupBanner.vue';
|
||||
import GroupHeaderDetails from '@/groups/partials/Page/GroupHeaderDetails.vue';
|
||||
import GroupNavTabs from '@/groups/partials/Page/GroupNavTabs.vue';
|
||||
import MemberOnlyWarning from '@/groups/partials/Membership/MemberOnlyWarning.vue';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
groupId: {
|
||||
type: String
|
||||
},
|
||||
|
||||
path: {
|
||||
type: String
|
||||
}
|
||||
},
|
||||
|
||||
components: {
|
||||
'status-card': StatusCard,
|
||||
'group-about': GroupAbout,
|
||||
'group-status': GroupStatus,
|
||||
'group-members': GroupMembers,
|
||||
'group-compose': GroupCompose,
|
||||
'group-topics': GroupTopics,
|
||||
'group-info-card': GroupInfoCard,
|
||||
'group-media': GroupMedia,
|
||||
'group-moderation': GroupModeration,
|
||||
'leave-group': LeaveGroup,
|
||||
'group-insights': GroupInsights,
|
||||
'search-modal': SearchModal,
|
||||
'invite-modal': InviteModal,
|
||||
'sidebar': SidebarComponent,
|
||||
'group-banner': GroupBanner,
|
||||
'group-header-details': GroupHeaderDetails,
|
||||
'group-nav-tabs': GroupNavTabs,
|
||||
'member-only-warning': MemberOnlyWarning
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
initialLoad: false,
|
||||
profile: undefined,
|
||||
group: {},
|
||||
isMember: false,
|
||||
isAdmin: false,
|
||||
renderIdx: 1,
|
||||
atabs: {
|
||||
moderation_count: 0,
|
||||
request_count: 0
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
created() {
|
||||
this.init();
|
||||
},
|
||||
|
||||
methods: {
|
||||
init() {
|
||||
this.initialLoad = false;
|
||||
axios.get('/api/pixelfed/v1/accounts/verify_credentials')
|
||||
.then(res => {
|
||||
this.profile = res.data;
|
||||
this.fetchGroup();
|
||||
})
|
||||
.catch(err => {
|
||||
window.location.href = '/login?_next=' + encodeURIComponent(window.location.href);
|
||||
});
|
||||
},
|
||||
|
||||
handleRefresh() {
|
||||
this.initialLoad = false;
|
||||
this.init();
|
||||
this.renderIdx++;
|
||||
},
|
||||
|
||||
fetchGroup() {
|
||||
axios.get('/api/v0/groups/' + this.groupId)
|
||||
.then(res => {
|
||||
this.group = res.data;
|
||||
this.isMember = res.data.self.is_member;
|
||||
this.isAdmin = ['founder', 'admin'].includes(res.data.self.role);
|
||||
|
||||
if(this.isAdmin) {
|
||||
this.fetchAdminTabs();
|
||||
}
|
||||
|
||||
this.initialLoad = true;
|
||||
})
|
||||
.catch(err => {
|
||||
//window.location.href = '/groups/unavailable';
|
||||
alert('error');
|
||||
});
|
||||
},
|
||||
|
||||
fetchAdminTabs() {
|
||||
axios.get('/api/v0/groups/' + this.groupId + '/atabs')
|
||||
.then(res => {
|
||||
this.atabs = res.data;
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.group-feed-component {
|
||||
&-body {
|
||||
min-height: 40vh;
|
||||
}
|
||||
}
|
||||
</style>
|
168
resources/assets/components/groups/Page/GroupMedia.vue
Normal file
168
resources/assets/components/groups/Page/GroupMedia.vue
Normal file
|
@ -0,0 +1,168 @@
|
|||
<template>
|
||||
<div class="group-feed-component">
|
||||
<div class="row border-bottom m-0 p-0">
|
||||
<sidebar />
|
||||
|
||||
<div class="col-12 col-md-9 px-md-0">
|
||||
<div class="bg-white mb-3 border-bottom">
|
||||
<div>
|
||||
<group-banner :group="group" />
|
||||
<group-header-details
|
||||
:group="group"
|
||||
:isAdmin="isAdmin"
|
||||
:isMember="isMember"
|
||||
@refresh="handleRefresh"
|
||||
/>
|
||||
<group-nav-tabs
|
||||
:group="group"
|
||||
:isAdmin="isAdmin"
|
||||
:isMember="isMember"
|
||||
:atabs="atabs"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="!initialLoad">
|
||||
<p class="text-center mt-5 pt-5 font-weight-bold">Loading...</p>
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<div class="container-xl group-feed-component-body">
|
||||
<template v-if="initialLoad && group.self.is_member">
|
||||
<group-media :key="renderIdx" :group="group" :profile="profile" />
|
||||
</template>
|
||||
|
||||
<member-only-warning v-else />
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script type="text/javascript">
|
||||
import StatusCard from '~/partials/StatusCard.vue';
|
||||
import GroupMembers from '@/groups/partials/GroupMembers.vue';
|
||||
import GroupCompose from '@/groups/partials/GroupCompose.vue';
|
||||
import GroupStatus from '@/groups/partials/GroupStatus.vue';
|
||||
import GroupAbout from '@/groups/partials/GroupAbout.vue';
|
||||
import GroupMedia from '@/groups/partials/GroupMedia.vue';
|
||||
import GroupModeration from '@/groups/partials/GroupModeration.vue';
|
||||
import GroupTopics from '@/groups/partials/GroupTopics.vue';
|
||||
import GroupInfoCard from '@/groups/partials/GroupInfoCard.vue';
|
||||
import LeaveGroup from '@/groups/partials/LeaveGroup.vue';
|
||||
import GroupInsights from '@/groups/partials/GroupInsights.vue';
|
||||
import SearchModal from '@/groups/partials/GroupSearchModal.vue';
|
||||
import InviteModal from '@/groups/partials/GroupInviteModal.vue';
|
||||
import SidebarComponent from '@/groups/sections/Sidebar.vue';
|
||||
import GroupBanner from '@/groups/partials/Page/GroupBanner.vue';
|
||||
import GroupHeaderDetails from '@/groups/partials/Page/GroupHeaderDetails.vue';
|
||||
import GroupNavTabs from '@/groups/partials/Page/GroupNavTabs.vue';
|
||||
import MemberOnlyWarning from '@/groups/partials/Membership/MemberOnlyWarning.vue';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
groupId: {
|
||||
type: String
|
||||
},
|
||||
|
||||
path: {
|
||||
type: String
|
||||
}
|
||||
},
|
||||
|
||||
components: {
|
||||
'status-card': StatusCard,
|
||||
'group-about': GroupAbout,
|
||||
'group-status': GroupStatus,
|
||||
'group-members': GroupMembers,
|
||||
'group-compose': GroupCompose,
|
||||
'group-topics': GroupTopics,
|
||||
'group-info-card': GroupInfoCard,
|
||||
'group-media': GroupMedia,
|
||||
'group-moderation': GroupModeration,
|
||||
'leave-group': LeaveGroup,
|
||||
'group-insights': GroupInsights,
|
||||
'search-modal': SearchModal,
|
||||
'invite-modal': InviteModal,
|
||||
'sidebar': SidebarComponent,
|
||||
'group-banner': GroupBanner,
|
||||
'group-header-details': GroupHeaderDetails,
|
||||
'group-nav-tabs': GroupNavTabs,
|
||||
'member-only-warning': MemberOnlyWarning
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
initialLoad: false,
|
||||
profile: undefined,
|
||||
group: {},
|
||||
isMember: false,
|
||||
isAdmin: false,
|
||||
renderIdx: 1,
|
||||
atabs: {
|
||||
moderation_count: 0,
|
||||
request_count: 0
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
created() {
|
||||
this.init();
|
||||
},
|
||||
|
||||
methods: {
|
||||
init() {
|
||||
this.initialLoad = false;
|
||||
axios.get('/api/pixelfed/v1/accounts/verify_credentials')
|
||||
.then(res => {
|
||||
this.profile = res.data;
|
||||
this.fetchGroup();
|
||||
})
|
||||
.catch(err => {
|
||||
window.location.href = '/login?_next=' + encodeURIComponent(window.location.href);
|
||||
});
|
||||
},
|
||||
|
||||
handleRefresh() {
|
||||
this.initialLoad = false;
|
||||
this.init();
|
||||
this.renderIdx++;
|
||||
},
|
||||
|
||||
fetchGroup() {
|
||||
axios.get('/api/v0/groups/' + this.groupId)
|
||||
.then(res => {
|
||||
this.group = res.data;
|
||||
this.isMember = res.data.self.is_member;
|
||||
this.isAdmin = ['founder', 'admin'].includes(res.data.self.role);
|
||||
|
||||
if(this.isAdmin) {
|
||||
this.fetchAdminTabs();
|
||||
}
|
||||
|
||||
this.initialLoad = true;
|
||||
})
|
||||
.catch(err => {
|
||||
//window.location.href = '/groups/unavailable';
|
||||
alert('error');
|
||||
});
|
||||
},
|
||||
|
||||
fetchAdminTabs() {
|
||||
axios.get('/api/v0/groups/' + this.groupId + '/atabs')
|
||||
.then(res => {
|
||||
this.atabs = res.data;
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.group-feed-component {
|
||||
&-body {
|
||||
min-height: 40vh;
|
||||
}
|
||||
}
|
||||
</style>
|
168
resources/assets/components/groups/Page/GroupMembers.vue
Normal file
168
resources/assets/components/groups/Page/GroupMembers.vue
Normal file
|
@ -0,0 +1,168 @@
|
|||
<template>
|
||||
<div class="group-feed-component">
|
||||
<div class="row border-bottom m-0 p-0">
|
||||
<sidebar />
|
||||
|
||||
<div class="col-12 col-md-9 px-md-0">
|
||||
<div class="bg-white mb-3 border-bottom">
|
||||
<div>
|
||||
<group-banner :group="group" />
|
||||
<group-header-details
|
||||
:group="group"
|
||||
:isAdmin="isAdmin"
|
||||
:isMember="isMember"
|
||||
@refresh="handleRefresh"
|
||||
/>
|
||||
<group-nav-tabs
|
||||
:group="group"
|
||||
:isAdmin="isAdmin"
|
||||
:isMember="isMember"
|
||||
:atabs="atabs"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="!initialLoad">
|
||||
<p class="text-center mt-5 pt-5 font-weight-bold">Loading...</p>
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<div class="container-xl group-feed-component-body">
|
||||
<template v-if="initialLoad && group.self.is_member">
|
||||
<group-members :key="renderIdx" :group="group" :profile="profile" />
|
||||
</template>
|
||||
|
||||
<member-only-warning v-else />
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script type="text/javascript">
|
||||
import StatusCard from '~/partials/StatusCard.vue';
|
||||
import GroupMembers from '@/groups/partials/GroupMembers.vue';
|
||||
import GroupCompose from '@/groups/partials/GroupCompose.vue';
|
||||
import GroupStatus from '@/groups/partials/GroupStatus.vue';
|
||||
import GroupAbout from '@/groups/partials/GroupAbout.vue';
|
||||
import GroupMedia from '@/groups/partials/GroupMedia.vue';
|
||||
import GroupModeration from '@/groups/partials/GroupModeration.vue';
|
||||
import GroupTopics from '@/groups/partials/GroupTopics.vue';
|
||||
import GroupInfoCard from '@/groups/partials/GroupInfoCard.vue';
|
||||
import LeaveGroup from '@/groups/partials/LeaveGroup.vue';
|
||||
import GroupInsights from '@/groups/partials/GroupInsights.vue';
|
||||
import SearchModal from '@/groups/partials/GroupSearchModal.vue';
|
||||
import InviteModal from '@/groups/partials/GroupInviteModal.vue';
|
||||
import SidebarComponent from '@/groups/sections/Sidebar.vue';
|
||||
import GroupBanner from '@/groups/partials/Page/GroupBanner.vue';
|
||||
import GroupHeaderDetails from '@/groups/partials/Page/GroupHeaderDetails.vue';
|
||||
import GroupNavTabs from '@/groups/partials/Page/GroupNavTabs.vue';
|
||||
import MemberOnlyWarning from '@/groups/partials/Membership/MemberOnlyWarning.vue';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
groupId: {
|
||||
type: String
|
||||
},
|
||||
|
||||
path: {
|
||||
type: String
|
||||
}
|
||||
},
|
||||
|
||||
components: {
|
||||
'status-card': StatusCard,
|
||||
'group-about': GroupAbout,
|
||||
'group-status': GroupStatus,
|
||||
'group-members': GroupMembers,
|
||||
'group-compose': GroupCompose,
|
||||
'group-topics': GroupTopics,
|
||||
'group-info-card': GroupInfoCard,
|
||||
'group-media': GroupMedia,
|
||||
'group-moderation': GroupModeration,
|
||||
'leave-group': LeaveGroup,
|
||||
'group-insights': GroupInsights,
|
||||
'search-modal': SearchModal,
|
||||
'invite-modal': InviteModal,
|
||||
'sidebar': SidebarComponent,
|
||||
'group-banner': GroupBanner,
|
||||
'group-header-details': GroupHeaderDetails,
|
||||
'group-nav-tabs': GroupNavTabs,
|
||||
'member-only-warning': MemberOnlyWarning
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
initialLoad: false,
|
||||
profile: undefined,
|
||||
group: {},
|
||||
isMember: false,
|
||||
isAdmin: false,
|
||||
renderIdx: 1,
|
||||
atabs: {
|
||||
moderation_count: 0,
|
||||
request_count: 0
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
created() {
|
||||
this.init();
|
||||
},
|
||||
|
||||
methods: {
|
||||
init() {
|
||||
this.initialLoad = false;
|
||||
axios.get('/api/pixelfed/v1/accounts/verify_credentials')
|
||||
.then(res => {
|
||||
this.profile = res.data;
|
||||
this.fetchGroup();
|
||||
})
|
||||
.catch(err => {
|
||||
window.location.href = '/login?_next=' + encodeURIComponent(window.location.href);
|
||||
});
|
||||
},
|
||||
|
||||
handleRefresh() {
|
||||
this.initialLoad = false;
|
||||
this.init();
|
||||
this.renderIdx++;
|
||||
},
|
||||
|
||||
fetchGroup() {
|
||||
axios.get('/api/v0/groups/' + this.groupId)
|
||||
.then(res => {
|
||||
this.group = res.data;
|
||||
this.isMember = res.data.self.is_member;
|
||||
this.isAdmin = ['founder', 'admin'].includes(res.data.self.role);
|
||||
|
||||
if(this.isAdmin) {
|
||||
this.fetchAdminTabs();
|
||||
}
|
||||
|
||||
this.initialLoad = true;
|
||||
})
|
||||
.catch(err => {
|
||||
//window.location.href = '/groups/unavailable';
|
||||
alert('error');
|
||||
});
|
||||
},
|
||||
|
||||
fetchAdminTabs() {
|
||||
axios.get('/api/v0/groups/' + this.groupId + '/atabs')
|
||||
.then(res => {
|
||||
this.atabs = res.data;
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.group-feed-component {
|
||||
&-body {
|
||||
min-height: 40vh;
|
||||
}
|
||||
}
|
||||
</style>
|
168
resources/assets/components/groups/Page/GroupTopics.vue
Normal file
168
resources/assets/components/groups/Page/GroupTopics.vue
Normal file
|
@ -0,0 +1,168 @@
|
|||
<template>
|
||||
<div class="group-feed-component">
|
||||
<div class="row border-bottom m-0 p-0">
|
||||
<sidebar />
|
||||
|
||||
<div class="col-12 col-md-9 px-md-0">
|
||||
<div class="bg-white mb-3 border-bottom">
|
||||
<div>
|
||||
<group-banner :group="group" />
|
||||
<group-header-details
|
||||
:group="group"
|
||||
:isAdmin="isAdmin"
|
||||
:isMember="isMember"
|
||||
@refresh="handleRefresh"
|
||||
/>
|
||||
<group-nav-tabs
|
||||
:group="group"
|
||||
:isAdmin="isAdmin"
|
||||
:isMember="isMember"
|
||||
:atabs="atabs"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="!initialLoad">
|
||||
<p class="text-center mt-5 pt-5 font-weight-bold">Loading...</p>
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<div class="container-xl group-feed-component-body">
|
||||
<template v-if="initialLoad && group.self.is_member">
|
||||
<group-topics :key="renderIdx" :group="group" :profile="profile" />
|
||||
</template>
|
||||
|
||||
<member-only-warning v-else />
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script type="text/javascript">
|
||||
import StatusCard from '~/partials/StatusCard.vue';
|
||||
import GroupMembers from '@/groups/partials/GroupMembers.vue';
|
||||
import GroupCompose from '@/groups/partials/GroupCompose.vue';
|
||||
import GroupStatus from '@/groups/partials/GroupStatus.vue';
|
||||
import GroupAbout from '@/groups/partials/GroupAbout.vue';
|
||||
import GroupMedia from '@/groups/partials/GroupMedia.vue';
|
||||
import GroupModeration from '@/groups/partials/GroupModeration.vue';
|
||||
import GroupTopics from '@/groups/partials/GroupTopics.vue';
|
||||
import GroupInfoCard from '@/groups/partials/GroupInfoCard.vue';
|
||||
import LeaveGroup from '@/groups/partials/LeaveGroup.vue';
|
||||
import GroupInsights from '@/groups/partials/GroupInsights.vue';
|
||||
import SearchModal from '@/groups/partials/GroupSearchModal.vue';
|
||||
import InviteModal from '@/groups/partials/GroupInviteModal.vue';
|
||||
import SidebarComponent from '@/groups/sections/Sidebar.vue';
|
||||
import GroupBanner from '@/groups/partials/Page/GroupBanner.vue';
|
||||
import GroupHeaderDetails from '@/groups/partials/Page/GroupHeaderDetails.vue';
|
||||
import GroupNavTabs from '@/groups/partials/Page/GroupNavTabs.vue';
|
||||
import MemberOnlyWarning from '@/groups/partials/Membership/MemberOnlyWarning.vue';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
groupId: {
|
||||
type: String
|
||||
},
|
||||
|
||||
path: {
|
||||
type: String
|
||||
}
|
||||
},
|
||||
|
||||
components: {
|
||||
'status-card': StatusCard,
|
||||
'group-about': GroupAbout,
|
||||
'group-status': GroupStatus,
|
||||
'group-members': GroupMembers,
|
||||
'group-compose': GroupCompose,
|
||||
'group-topics': GroupTopics,
|
||||
'group-info-card': GroupInfoCard,
|
||||
'group-media': GroupMedia,
|
||||
'group-moderation': GroupModeration,
|
||||
'leave-group': LeaveGroup,
|
||||
'group-insights': GroupInsights,
|
||||
'search-modal': SearchModal,
|
||||
'invite-modal': InviteModal,
|
||||
'sidebar': SidebarComponent,
|
||||
'group-banner': GroupBanner,
|
||||
'group-header-details': GroupHeaderDetails,
|
||||
'group-nav-tabs': GroupNavTabs,
|
||||
'member-only-warning': MemberOnlyWarning
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
initialLoad: false,
|
||||
profile: undefined,
|
||||
group: {},
|
||||
isMember: false,
|
||||
isAdmin: false,
|
||||
renderIdx: 1,
|
||||
atabs: {
|
||||
moderation_count: 0,
|
||||
request_count: 0
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
created() {
|
||||
this.init();
|
||||
},
|
||||
|
||||
methods: {
|
||||
init() {
|
||||
this.initialLoad = false;
|
||||
axios.get('/api/pixelfed/v1/accounts/verify_credentials')
|
||||
.then(res => {
|
||||
this.profile = res.data;
|
||||
this.fetchGroup();
|
||||
})
|
||||
.catch(err => {
|
||||
window.location.href = '/login?_next=' + encodeURIComponent(window.location.href);
|
||||
});
|
||||
},
|
||||
|
||||
handleRefresh() {
|
||||
this.initialLoad = false;
|
||||
this.init();
|
||||
this.renderIdx++;
|
||||
},
|
||||
|
||||
fetchGroup() {
|
||||
axios.get('/api/v0/groups/' + this.groupId)
|
||||
.then(res => {
|
||||
this.group = res.data;
|
||||
this.isMember = res.data.self.is_member;
|
||||
this.isAdmin = ['founder', 'admin'].includes(res.data.self.role);
|
||||
|
||||
if(this.isAdmin) {
|
||||
this.fetchAdminTabs();
|
||||
}
|
||||
|
||||
this.initialLoad = true;
|
||||
})
|
||||
.catch(err => {
|
||||
//window.location.href = '/groups/unavailable';
|
||||
alert('error');
|
||||
});
|
||||
},
|
||||
|
||||
fetchAdminTabs() {
|
||||
axios.get('/api/v0/groups/' + this.groupId + '/atabs')
|
||||
.then(res => {
|
||||
this.atabs = res.data;
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.group-feed-component {
|
||||
&-body {
|
||||
min-height: 40vh;
|
||||
}
|
||||
}
|
||||
</style>
|
841
resources/assets/components/groups/partials/CommentDrawer.vue
Normal file
841
resources/assets/components/groups/partials/CommentDrawer.vue
Normal file
|
@ -0,0 +1,841 @@
|
|||
<template>
|
||||
<div class="comment-drawer-component">
|
||||
<input type="file" ref="fileInput" class="d-none" accept="image/jpeg,image/png" @change="handleImageUpload">
|
||||
<div v-if="hide"></div>
|
||||
<div v-else-if="!isLoaded" class="border-top d-flex justify-content-center py-3">
|
||||
<div class="text-center">
|
||||
<div
|
||||
class="spinner-border text-lighter"
|
||||
role="status">
|
||||
<span class="sr-only">Loading...</span>
|
||||
</div>
|
||||
<p class="text-muted">Loading Comments ...</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="border-top">
|
||||
<!-- <div v-if="profile && canReply" class="my-3">
|
||||
<div class="d-flex align-items-top reply-form">
|
||||
<img class="rounded-circle mr-2 border" :src="avatar" width="38" height="38">
|
||||
<div v-if="isUploading" class="w-100">
|
||||
<p class="font-weight-light mb-1">Uploading image ...</p>
|
||||
<div class="progress rounded-pill" style="height:4px">
|
||||
<div class="progress-bar" role="progressbar" :aria-valuenow="uploadProgress" aria-valuemin="0" aria-valuemax="100" :style="{ width: uploadProgress + '%'}"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="reply-form-input">
|
||||
<input
|
||||
class="form-control bg-light border-lighter rounded-pill"
|
||||
placeholder="Write a comment...."
|
||||
v-model="replyContent"
|
||||
v-on:keyup.enter="storeComment">
|
||||
|
||||
<div class="reply-form-input-actions">
|
||||
<button
|
||||
class="btn btn-link text-muted px-1 mr-2"
|
||||
@click="uploadImage">
|
||||
<i class="far fa-image fa-lg"></i>
|
||||
</button>
|
||||
<button class="btn btn-link text-muted px-1 small font-weight-bold border py-0 rounded-pill text-decoration-none">
|
||||
GIF
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
<div class="my-3">
|
||||
<div
|
||||
v-for="(status, index) in feed"
|
||||
:key="'cdf' + index + status.id"
|
||||
class="media media-status align-items-top">
|
||||
|
||||
<a
|
||||
v-if="replyChildId == status.id"
|
||||
href="#comment-1"
|
||||
class="comment-border-link"
|
||||
@click.prevent="replyToChild(status)">
|
||||
<span class="sr-only">Jump to comment-{{ index }}</span>
|
||||
</a>
|
||||
|
||||
<a :href="status.account.url">
|
||||
<img class="rounded-circle media-avatar border" :src="status.account.avatar" width="32" height="32" onerror="this.onerror=null;this.src='/storage/avatars/default.png?v=2'" />
|
||||
</a>
|
||||
|
||||
<div class="media-body">
|
||||
<div v-if="!status.media_attachments.length" class="media-body-comment">
|
||||
<p class="media-body-comment-username">
|
||||
<a :href="status.account.url">
|
||||
{{status.account.acct}}
|
||||
</a>
|
||||
</p>
|
||||
<read-more :status="status" />
|
||||
</div>
|
||||
<div v-else>
|
||||
<p class="media-body-comment-username">
|
||||
<a :href="status.account.url">
|
||||
{{status.account.acct}}
|
||||
</a>
|
||||
</p>
|
||||
<div class="bh-comment" @click="lightbox(status)">
|
||||
<blur-hash-image
|
||||
:width="blurhashWidth(status)"
|
||||
:height="blurhashHeight(status)"
|
||||
:punch="1"
|
||||
class="img-fluid rounded-lg border shadow"
|
||||
:hash="status.media_attachments[0].blurhash"
|
||||
:src="getMediaSource(status)" />
|
||||
</div>
|
||||
</div>
|
||||
<p class="media-body-reactions">
|
||||
<a
|
||||
v-if="profile"
|
||||
href="#"
|
||||
class="font-weight-bold"
|
||||
:class="[ status.favourited ? 'text-primary' : 'text-muted' ]"
|
||||
@click.prevent="likeComment(status, index, $event)">
|
||||
{{ status.favourited ? 'Liked' : 'Like' }}
|
||||
</a>
|
||||
<span class="mx-1">·</span>
|
||||
<a href="#" class="text-muted font-weight-bold" @click.prevent="replyToChild(status, index)">Reply</a>
|
||||
<span v-if="profile" class="mx-1">·</span>
|
||||
<a
|
||||
class="font-weight-bold text-muted"
|
||||
:href="status.url"
|
||||
v-once>
|
||||
{{ shortTimestamp(status.created_at) }}
|
||||
</a>
|
||||
<span v-if="profile && status.account.id === profile.id">
|
||||
<span class="mx-1">·</span>
|
||||
<a
|
||||
class="font-weight-bold text-lighter"
|
||||
href="#"
|
||||
@click.prevent="deleteComment(index)">
|
||||
Delete
|
||||
</a>
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<!-- <div v-if="replyChildId == status.id && status.reply_count" class="media media-status align-items-top mt-3">
|
||||
<div class="comment-border-arrow"></div>
|
||||
<a href="https://pixelfed.test/groups/328821658771132416/user/321493203255693312"><img src="https://pixelfed.test/storage/avatars/321493203255693312/5a6nqo.jpg?v=2" width="32" height="32" class="rounded-circle media-avatar border"></a>
|
||||
<div class="media-body"><div class="media-body-comment"><p class="media-body-comment-username"><a href="https://pixelfed.test/groups/328821658771132416/user/321493203255693312">
|
||||
dansup
|
||||
</a></p> <div class="read-more-component" style="word-break: break-all;"><div>test</div></div></div> <p class="media-body-reactions"><a href="#" class="font-weight-bold text-muted">
|
||||
Like
|
||||
</a> <span class="mx-1">·</span> <a href="https://pixelfed.test/groups/328821658771132416/p/358529382599041029" class="font-weight-bold text-muted">
|
||||
1h
|
||||
</a> <span><span class="mx-1">·</span> <a href="#" class="font-weight-bold text-lighter">
|
||||
Delete
|
||||
</a></span></p>
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<div v-if="replyChildIndex == index && status.hasOwnProperty('children') && status.children.hasOwnProperty('feed') && status.children.feed.length">
|
||||
<comment-post
|
||||
v-for="(s, index) in status.children.feed"
|
||||
:status="s"
|
||||
:profile="profile"
|
||||
:commentBorderArrow="true"
|
||||
:key="'scp_'+index+'_'+s.id"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<a
|
||||
v-if="replyChildIndex == index &&
|
||||
status.hasOwnProperty('children') &&
|
||||
status.children.hasOwnProperty('can_load_more') &&
|
||||
status.children.can_load_more == true"
|
||||
class="text-muted font-weight-bold mt-1 mb-0"
|
||||
style="font-size: 13px;"
|
||||
href="#"
|
||||
:disabled="loadingChildComments"
|
||||
@click.prevent="loadMoreChildComments(status, index)">
|
||||
<div class="comment-border-arrow"></div>
|
||||
<i class="far fa-long-arrow-right mr-1"></i>
|
||||
{{ loadingChildComments ? 'Loading' : 'Load' }} more comments
|
||||
</a>
|
||||
|
||||
<a
|
||||
v-else-if="replyChildIndex !== index &&
|
||||
status.hasOwnProperty('children') &&
|
||||
status.children.hasOwnProperty('can_load_more') &&
|
||||
status.children.can_load_more == true &&
|
||||
status.reply_count > 0 &&
|
||||
!loadingChildComments"
|
||||
class="text-muted font-weight-bold mt-1 mb-0"
|
||||
style="font-size: 13px;"
|
||||
href="#"
|
||||
:disabled="loadingChildComments"
|
||||
@click.prevent="replyToChild(status, index)">
|
||||
<i class="far fa-long-arrow-right mr-1"></i>
|
||||
{{ loadingChildComments ? 'Loading' : 'Load' }} more comments
|
||||
</a>
|
||||
|
||||
<div v-if="replyChildId == status.id" class="mt-3 mb-3 d-flex align-items-top reply-form child-reply-form">
|
||||
<div class="comment-border-arrow"></div>
|
||||
<img class="rounded-circle mr-2 border" :src="avatar" width="38" height="38" onerror="this.onerror=null;this.src='/storage/avatars/default.png?v=2'">
|
||||
<div v-if="isUploading" class="w-100">
|
||||
<p class="font-weight-light mb-1">Uploading image ...</p>
|
||||
<div class="progress rounded-pill" style="height:10px">
|
||||
<div class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" :aria-valuenow="uploadProgress" aria-valuemin="0" aria-valuemax="100" :style="{ width: uploadProgress + '%'}"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="reply-form-input">
|
||||
<input
|
||||
class="form-control bg-light border-lighter rounded-pill"
|
||||
placeholder="Write a comment...."
|
||||
v-model="childReplyContent"
|
||||
:disabled="postingChildComment"
|
||||
v-on:keyup.enter="storeChildComment(index)">
|
||||
|
||||
<!-- <div class="reply-form-input-actions">
|
||||
<button
|
||||
class="btn btn-link text-muted px-1 mr-2"
|
||||
@click="uploadImage">
|
||||
<i class="far fa-image fa-lg"></i>
|
||||
</button>
|
||||
<button class="btn btn-link text-muted px-1 small font-weight-bold border py-0 rounded-pill text-decoration-none">
|
||||
GIF
|
||||
</button>
|
||||
</div> -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<!-- <a v-if="permalinkMode && canLoadMore" class="text-muted mb-n1" style="font-size: 13px;font-weight: 600;" href="#">Load more comments ...</a> -->
|
||||
</div>
|
||||
<button
|
||||
v-if="canLoadMore"
|
||||
class="btn btn-link btn-sm text-muted mb-2"
|
||||
@click="loadMoreComments"
|
||||
:disabled="isLoadingMore">
|
||||
|
||||
<span v-if="!isLoadingMore">
|
||||
Load more comments ...
|
||||
</span>
|
||||
<div
|
||||
v-else
|
||||
class="spinner-border spinner-border-sm text-muted"
|
||||
role="status">
|
||||
<span class="sr-only">Loading...</span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<div v-if="profile && canReply" class="mt-3 mb-n3">
|
||||
<div class="d-flex align-items-top reply-form cdrawer-reply-form">
|
||||
<img class="rounded-circle mr-2 border" :src="avatar" width="38" height="38" onerror="this.onerror=null;this.src='/storage/avatars/default.png?v=2'">
|
||||
<div v-if="isUploading" class="w-100">
|
||||
<p class="font-weight-light small text-muted mb-1">Uploading image ...</p>
|
||||
<div class="progress rounded-pill" style="height:10px">
|
||||
<div class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" :aria-valuenow="uploadProgress" aria-valuemin="0" aria-valuemax="100" :style="{ width: uploadProgress + '%'}"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="w-100">
|
||||
<div class="reply-form-input">
|
||||
<textarea
|
||||
class="form-control bg-light border-lighter"
|
||||
placeholder="Write a comment...."
|
||||
:rows="replyContent && replyContent.length > 40 ? 4 : 1"
|
||||
v-model="replyContent"></textarea>
|
||||
|
||||
<div class="reply-form-input-actions">
|
||||
<button
|
||||
class="btn btn-link text-muted px-1 mr-2"
|
||||
@click="uploadImage">
|
||||
<i class="far fa-image fa-lg"></i>
|
||||
</button>
|
||||
<!-- <button class="btn btn-link text-muted px-1 small font-weight-bold border py-0 rounded-pill text-decoration-none">
|
||||
GIF
|
||||
</button> -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between reply-form-menu">
|
||||
<div class="char-counter">
|
||||
<span>{{ replyContent?.length ?? 0 }}</span>
|
||||
<span>/</span>
|
||||
<span>500</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
class="btn btn-link btn-sm font-weight-bold align-self-center ml-3 mb-3"
|
||||
@click="storeComment">Post</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<b-modal ref="lightboxModal"
|
||||
id="lightbox"
|
||||
:hide-header="true"
|
||||
:hide-footer="true"
|
||||
centered
|
||||
size="lg"
|
||||
body-class="p-0"
|
||||
content-class="bg-transparent border-0"
|
||||
>
|
||||
<div v-if="lightboxStatus" @click="hideLightbox">
|
||||
<img :src="lightboxStatus.url" style="width: 100%;max-height: 90vh;object-fit: contain;">
|
||||
</div>
|
||||
</b-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script type="text/javascript">
|
||||
import ReadMore from './ReadMore.vue';
|
||||
import CommentPost from './CommentPost.vue';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
groupId: {
|
||||
type: String
|
||||
},
|
||||
|
||||
profile: {
|
||||
type: Object
|
||||
},
|
||||
|
||||
status: {
|
||||
type: Object
|
||||
},
|
||||
|
||||
show: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
|
||||
permalinkMode: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
|
||||
permalinkStatus: {
|
||||
type: Object
|
||||
},
|
||||
|
||||
canReply: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
|
||||
components: {
|
||||
"read-more": ReadMore,
|
||||
"comment-post": CommentPost
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
isLoaded: false,
|
||||
hide: false,
|
||||
feed: [],
|
||||
canLoadMore: false,
|
||||
isLoadingMore: false,
|
||||
replyContent: null,
|
||||
maxReplyId: null,
|
||||
readMoreCursor: 200,
|
||||
avatar: '/storage/avatars/default.png',
|
||||
isUploading: false,
|
||||
uploadProgress: 0,
|
||||
lightboxStatus: null,
|
||||
replyChildId: undefined,
|
||||
replyChildIndex: undefined,
|
||||
childReplyContent: null,
|
||||
postingChildComment: false,
|
||||
loadingChildComments: false,
|
||||
replyChildMinId: undefined
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
if(this.permalinkMode && this.permalinkStatus) {
|
||||
let status = this.permalinkStatus;
|
||||
if(status.reply_count) {
|
||||
status.children = {
|
||||
feed: [],
|
||||
can_load_more: true
|
||||
}
|
||||
}
|
||||
this.feed.push(status);
|
||||
this.isLoaded = true;
|
||||
this.canLoadMore = false;
|
||||
} else {
|
||||
this.fetchComments();
|
||||
}
|
||||
|
||||
if(this.profile && this.profile.hasOwnProperty('avatar')) {
|
||||
this.avatar = this.profile.avatar;
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
fetchComments() {
|
||||
axios.get('/api/v0/groups/comments', {
|
||||
params: {
|
||||
gid: this.groupId,
|
||||
sid: this.status.id,
|
||||
limit: 3
|
||||
}
|
||||
}).then(res => {
|
||||
let data = res.data.map(function(c) {
|
||||
if(c.reply_count && c.reply_count > 0) {
|
||||
c.children = {
|
||||
feed: [],
|
||||
can_load_more: true
|
||||
}
|
||||
}
|
||||
return c;
|
||||
})
|
||||
this.feed = data;
|
||||
this.isLoaded = true;
|
||||
this.maxReplyId = res.data[(res.data.length - 1)].id;
|
||||
if(this.feed.length == 3) {
|
||||
this.canLoadMore = true;
|
||||
} else {
|
||||
}
|
||||
}).catch(err => {
|
||||
this.isLoaded = true;
|
||||
})
|
||||
},
|
||||
|
||||
loadMoreComments() {
|
||||
this.isLoadingMore = true;
|
||||
|
||||
axios.get('/api/v0/groups/comments', {
|
||||
params: {
|
||||
gid: this.groupId,
|
||||
sid: this.status.id,
|
||||
limit: 3,
|
||||
max_id: this.maxReplyId
|
||||
}
|
||||
}).then(res => {
|
||||
if(res.data[res.data.length - 1].id == this.maxReplyId) {
|
||||
this.isLoadingMore = false;
|
||||
this.canLoadMore = false;
|
||||
return;
|
||||
}
|
||||
this.feed.push(...res.data);
|
||||
setTimeout(() => {
|
||||
this.isLoadingMore = false;
|
||||
}, 500);
|
||||
this.maxReplyId = res.data[res.data.length - 1].id;
|
||||
if(res.data.length > 0) {
|
||||
this.canLoadMore = true;
|
||||
} else {
|
||||
this.canLoadMore = false;
|
||||
}
|
||||
}).catch(err => {
|
||||
this.isLoadingMore = false;
|
||||
this.canLoadMore = false;
|
||||
})
|
||||
},
|
||||
|
||||
storeComment($event) {
|
||||
$event.currentTarget?.blur();
|
||||
axios.post('/api/v0/groups/comment', {
|
||||
gid: this.groupId,
|
||||
sid: this.status.id,
|
||||
content: this.replyContent
|
||||
})
|
||||
.then(res => {
|
||||
this.replyContent = null;
|
||||
this.feed.unshift(res.data);
|
||||
}).catch(err => {
|
||||
if(err.response.status == 422) {
|
||||
this.isUploading = false;
|
||||
this.uploadProgress = 0;
|
||||
swal('Oops!', err.response.data.error, 'error');
|
||||
} else {
|
||||
this.isUploading = false;
|
||||
this.uploadProgress = 0;
|
||||
swal('Oops!', 'An error occured while processing your request, please try again later', 'error');
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
shortTimestamp(ts) {
|
||||
return window.App.util.format.timeAgo(ts);
|
||||
},
|
||||
|
||||
readMore() {
|
||||
this.readMoreCursor = this.readMoreCursor + 200;
|
||||
},
|
||||
|
||||
likeComment(status, index, $event) {
|
||||
$event.target.blur();
|
||||
let l = status.favourited ? false : true;
|
||||
this.feed[index].favourited = l;
|
||||
status.favourited = l;
|
||||
axios.post(`/api/v0/groups/comment/${l ? 'like' : 'unlike'}`, {
|
||||
sid: status.id,
|
||||
gid: this.groupId
|
||||
});
|
||||
},
|
||||
|
||||
deleteComment(index) {
|
||||
if(window.confirm('Are you sure you want to delete this post?') == false) {
|
||||
return;
|
||||
}
|
||||
|
||||
axios.post('/api/v0/groups/comment/delete', {
|
||||
gid: this.groupId,
|
||||
id: this.feed[index].id
|
||||
}).then(res => {
|
||||
this.feed.splice(index, 1);
|
||||
}).catch(err => {
|
||||
console.log(err.response);
|
||||
swal('Error', 'Something went wrong. Please try again later.', 'error');
|
||||
});
|
||||
},
|
||||
|
||||
uploadImage() {
|
||||
this.$refs.fileInput.click();
|
||||
},
|
||||
|
||||
handleImageUpload() {
|
||||
if(!this.$refs.fileInput.files.length) {
|
||||
return;
|
||||
}
|
||||
this.isUploading = true;
|
||||
let self = this;
|
||||
let data = new FormData();
|
||||
data.append('gid', this.groupId);
|
||||
data.append('sid', this.status.id);
|
||||
data.append('photo', this.$refs.fileInput.files[0]);
|
||||
|
||||
axios.post('/api/v0/groups/comment/photo', data, {
|
||||
onUploadProgress: function(progressEvent) {
|
||||
self.uploadProgress = Math.floor(progressEvent.loaded / progressEvent.total * 100);
|
||||
}
|
||||
})
|
||||
.then(res => {
|
||||
this.isUploading = false;
|
||||
this.uploadProgress = 0;
|
||||
this.feed.unshift(res.data);
|
||||
}).catch(err => {
|
||||
if(err.response.status == 422) {
|
||||
this.isUploading = false;
|
||||
this.uploadProgress = 0;
|
||||
swal('Oops!', err.response.data.error, 'error');
|
||||
} else {
|
||||
this.isUploading = false;
|
||||
this.uploadProgress = 0;
|
||||
swal('Oops!', 'An error occured while processing your request, please try again later', 'error');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
lightbox(status) {
|
||||
this.lightboxStatus = status.media_attachments[0];
|
||||
this.$refs.lightboxModal.show();
|
||||
},
|
||||
|
||||
hideLightbox() {
|
||||
this.lightboxStatus = null;
|
||||
this.$refs.lightboxModal.hide();
|
||||
},
|
||||
|
||||
blurhashWidth(status) {
|
||||
if(!status.media_attachments[0].meta) {
|
||||
return 25;
|
||||
}
|
||||
let aspect = status.media_attachments[0].meta.original.aspect;
|
||||
if(aspect == 1) {
|
||||
return 25;
|
||||
} else if(aspect > 1) {
|
||||
return 30;
|
||||
} else {
|
||||
return 20;
|
||||
}
|
||||
},
|
||||
|
||||
blurhashHeight(status) {
|
||||
if(!status.media_attachments[0].meta) {
|
||||
return 25;
|
||||
}
|
||||
let aspect = status.media_attachments[0].meta.original.aspect;
|
||||
if(aspect == 1) {
|
||||
return 25;
|
||||
} else if(aspect > 1) {
|
||||
return 20;
|
||||
} else {
|
||||
return 30;
|
||||
}
|
||||
},
|
||||
|
||||
getMediaSource(status) {
|
||||
let media = status.media_attachments[0];
|
||||
|
||||
if(media.preview_url.endsWith('storage/no-preview.png')) {
|
||||
return media.url;
|
||||
}
|
||||
|
||||
return media.preview_url;
|
||||
},
|
||||
|
||||
replyToChild(status, index) {
|
||||
|
||||
if(this.replyChildId == status.id) {
|
||||
this.replyChildId = null;
|
||||
this.replyChildIndex = null;
|
||||
return;
|
||||
} else {
|
||||
this.childReplyContent = null;
|
||||
}
|
||||
|
||||
this.replyChildId = status.id;
|
||||
this.replyChildIndex = index;
|
||||
|
||||
if(!status.hasOwnProperty('replies_loaded') || !status.replies_loaded) {
|
||||
this.$nextTick(() => {
|
||||
this.fetchChildReplies(status, index);
|
||||
});
|
||||
} else {
|
||||
|
||||
}
|
||||
},
|
||||
|
||||
fetchChildReplies(status, index) {
|
||||
axios.get('/api/v0/groups/comments', {
|
||||
params: {
|
||||
gid: this.groupId,
|
||||
sid: status.id,
|
||||
cid: 1,
|
||||
limit: 3
|
||||
}
|
||||
}).then(res => {
|
||||
if(this.feed[index].hasOwnProperty('children')) {
|
||||
this.feed[index].children.feed.push(res.data);
|
||||
this.feed[index].children.can_load_more = res.data.length == 3;
|
||||
} else {
|
||||
this.feed[index].children = {
|
||||
feed: res.data,
|
||||
can_load_more: res.data.length == 3
|
||||
}
|
||||
}
|
||||
this.replyChildMinId = res.data[res.data.length - 1].id;
|
||||
this.$nextTick(() => {
|
||||
this.feed[index].replies_loaded = true;
|
||||
});
|
||||
}).catch(err => {
|
||||
this.feed[index].children.can_load_more = false;
|
||||
})
|
||||
},
|
||||
|
||||
storeChildComment(index) {
|
||||
this.postingChildComment = true;
|
||||
|
||||
axios.post('/api/v0/groups/comment', {
|
||||
gid: this.groupId,
|
||||
sid: this.status.id,
|
||||
cid: this.replyChildId,
|
||||
content: this.childReplyContent
|
||||
})
|
||||
.then(res => {
|
||||
this.childReplyContent = null;
|
||||
this.postingChildComment = false;
|
||||
this.feed[index].children.feed.push(res.data);
|
||||
// this.feed.unshift(res.data);
|
||||
}).catch(err => {
|
||||
if(err.response.status == 422) {
|
||||
// this.isUploading = false;
|
||||
// this.uploadProgress = 0;
|
||||
swal('Oops!', err.response.data.error, 'error');
|
||||
} else {
|
||||
// this.isUploading = false;
|
||||
// this.uploadProgress = 0;
|
||||
swal('Oops!', 'An error occured while processing your request, please try again later', 'error');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
loadMoreChildComments(status, index) {
|
||||
this.loadingChildComments = true;
|
||||
|
||||
axios.get('/api/v0/groups/comments', {
|
||||
params: {
|
||||
gid: this.groupId,
|
||||
sid: status.id,
|
||||
max_id: this.replyChildMinId,
|
||||
cid: 1,
|
||||
limit: 3
|
||||
}
|
||||
}).then(res => {
|
||||
if(this.feed[index].hasOwnProperty('children')) {
|
||||
this.feed[index].children.feed.push(...res.data);
|
||||
this.feed[index].children.can_load_more = res.data.length == 3;
|
||||
} else {
|
||||
this.feed[index].children = {
|
||||
feed: res.data,
|
||||
can_load_more: res.data.length == 3
|
||||
}
|
||||
}
|
||||
this.replyChildMinId = res.data[res.data.length - 1].id;
|
||||
this.feed[index].replies_loaded = true;
|
||||
this.loadingChildComments = false;
|
||||
}).catch(err => {
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.comment-drawer-component {
|
||||
.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;
|
||||
}
|
||||
|
||||
&-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: #000;
|
||||
|
||||
a {
|
||||
color: #000;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
&-content {
|
||||
margin-bottom: 0;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
&-reactions {
|
||||
margin-top: 0.25rem !important;
|
||||
margin-bottom: 0 !important;
|
||||
color: #B8C2CC !important;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.load-more-comments {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.reply-form {
|
||||
margin-bottom: 2rem;
|
||||
|
||||
&-input {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
|
||||
textarea {
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
resize: none;
|
||||
padding-right: 100px;
|
||||
}
|
||||
|
||||
&-actions {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
}
|
||||
|
||||
.btn {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
&-menu {
|
||||
margin-top: 5px;
|
||||
|
||||
.char-counter {
|
||||
color: var(--muted);
|
||||
font-size: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.bh-comment {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
max-width: 160px !important;
|
||||
max-height: 260px !important;
|
||||
|
||||
span {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
max-width: 160px !important;
|
||||
max-height: 260px !important;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
max-width: 160px !important;
|
||||
max-height: 260px !important;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
405
resources/assets/components/groups/partials/CommentPost.vue
Normal file
405
resources/assets/components/groups/partials/CommentPost.vue
Normal file
|
@ -0,0 +1,405 @@
|
|||
<template>
|
||||
<div class="comment-post-component">
|
||||
<div class="media media-status align-items-top mt-3">
|
||||
<div v-if="commentBorderArrow" class="comment-border-arrow"></div>
|
||||
<!-- <a
|
||||
v-if="replyChildId == status.id"
|
||||
href="#comment-1"
|
||||
class="comment-border-link"
|
||||
@click.prevent="replyToChild(status)">
|
||||
<span class="sr-only">Jump to comment-{{ index }}</span>
|
||||
</a> -->
|
||||
|
||||
<a :href="status.account.url">
|
||||
<img class="rounded-circle media-avatar border" :src="status.account.avatar" width="32" height="32" onerror="this.onerror=null;this.src='/storage/avatars/default.png?v=2'">
|
||||
</a>
|
||||
|
||||
<div class="media-body">
|
||||
<div v-if="!status.media_attachments.length" class="media-body-comment">
|
||||
<p class="media-body-comment-username">
|
||||
<a :href="status.account.url">
|
||||
{{status.account.acct}}
|
||||
</a>
|
||||
</p>
|
||||
<read-more :status="status" />
|
||||
</div>
|
||||
<div v-else>
|
||||
<p class="media-body-comment-username">
|
||||
<a :href="status.account.url">
|
||||
{{status.account.acct}}
|
||||
</a>
|
||||
</p>
|
||||
<div class="bh-comment" @click="lightbox(status)">
|
||||
<blur-hash-image
|
||||
:width="blurhashWidth(status)"
|
||||
:height="blurhashHeight(status)"
|
||||
:punch="1"
|
||||
class="img-fluid rounded-lg border shadow"
|
||||
:hash="status.media_attachments[0].blurhash"
|
||||
:src="getMediaSource(status)" />
|
||||
</div>
|
||||
</div>
|
||||
<p class="media-body-reactions">
|
||||
<a
|
||||
v-if="profile"
|
||||
href="#"
|
||||
class="font-weight-bold"
|
||||
:class="[ status.favourited ? 'text-primary' : 'text-muted' ]"
|
||||
@click.prevent="likeComment(status, index, $event)">
|
||||
{{ status.favourited ? 'Liked' : 'Like' }}
|
||||
</a>
|
||||
<!-- <span class="mx-1">·</span> -->
|
||||
<!-- <a href="#" class="text-muted font-weight-bold" @click.prevent="replyToChild(status, index)">Reply</a> -->
|
||||
<span v-if="profile" class="mx-1">·</span>
|
||||
<a
|
||||
class="font-weight-bold text-muted"
|
||||
:href="status.url"
|
||||
v-once>
|
||||
{{ shortTimestamp(status.created_at) }}
|
||||
</a>
|
||||
<span v-if="profile && status.account.id === profile.id">
|
||||
<span class="mx-1">·</span>
|
||||
<a
|
||||
class="font-weight-bold text-lighter"
|
||||
href="#"
|
||||
@click.prevent="deleteComment(index)">
|
||||
Delete
|
||||
</a>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script type="text/javascript">
|
||||
import ReadMore from './ReadMore.vue';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
groupId: {
|
||||
type: String
|
||||
},
|
||||
|
||||
profile: {
|
||||
type: Object
|
||||
},
|
||||
|
||||
status: {
|
||||
type: Object,
|
||||
},
|
||||
|
||||
commentBorderArrow: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
components: {
|
||||
"read-more": ReadMore
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
isLoaded: false,
|
||||
hide: false,
|
||||
feed: [],
|
||||
canLoadMore: false,
|
||||
isLoadingMore: false,
|
||||
replyContent: null,
|
||||
maxReplyId: null,
|
||||
readMoreCursor: 200,
|
||||
avatar: '/storage/avatars/default.png',
|
||||
isUploading: false,
|
||||
uploadProgress: 0,
|
||||
lightboxStatus: null,
|
||||
replyChildId: undefined,
|
||||
childReplyContent: null,
|
||||
postingChildComment: false
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
console.log(this.status);
|
||||
// if(this.permalinkMode && this.permalinkStatus) {
|
||||
// this.feed.push(this.permalinkStatus);
|
||||
// this.isLoaded = true;
|
||||
// this.canLoadMore = false;
|
||||
// } else {
|
||||
// this.fetchComments();
|
||||
// }
|
||||
|
||||
// if(this.profile && this.profile.hasOwnProperty('avatar')) {
|
||||
// this.avatar = this.profile.avatar;
|
||||
// }
|
||||
},
|
||||
|
||||
methods: {
|
||||
fetchComments() {
|
||||
axios.get('/api/v0/groups/comments', {
|
||||
params: {
|
||||
gid: this.groupId,
|
||||
sid: this.status.id,
|
||||
limit: 3
|
||||
}
|
||||
}).then(res => {
|
||||
this.feed = res.data;
|
||||
this.isLoaded = true;
|
||||
this.maxReplyId = res.data[(res.data.length - 1)].id;
|
||||
if(this.feed.length == 3) {
|
||||
this.canLoadMore = true;
|
||||
}
|
||||
}).catch(err => {
|
||||
this.isLoaded = true;
|
||||
})
|
||||
},
|
||||
|
||||
loadMoreComments() {
|
||||
this.isLoadingMore = true;
|
||||
|
||||
axios.get('/api/v0/groups/comments', {
|
||||
params: {
|
||||
gid: this.groupId,
|
||||
sid: this.status.id,
|
||||
limit: 3,
|
||||
max_id: this.maxReplyId
|
||||
}
|
||||
}).then(res => {
|
||||
if(res.data[res.data.length - 1].id == this.maxReplyId) {
|
||||
this.isLoadingMore = false;
|
||||
this.canLoadMore = false;
|
||||
return;
|
||||
}
|
||||
this.feed.push(...res.data);
|
||||
this.isLoadingMore = false;
|
||||
this.maxReplyId = res.data[res.data.length - 1].id;
|
||||
if(res.data.length > 0) {
|
||||
this.canLoadMore = true;
|
||||
} else {
|
||||
this.canLoadMore = false;
|
||||
}
|
||||
}).catch(err => {
|
||||
this.isLoadingMore = false;
|
||||
this.canLoadMore = false;
|
||||
})
|
||||
},
|
||||
|
||||
storeComment() {
|
||||
axios.post('/api/v0/groups/comment', {
|
||||
gid: this.groupId,
|
||||
sid: this.status.id,
|
||||
content: this.replyContent
|
||||
})
|
||||
.then(res => {
|
||||
this.replyContent = null;
|
||||
this.feed.unshift(res.data);
|
||||
}).catch(err => {
|
||||
if(err.response.status == 422) {
|
||||
this.isUploading = false;
|
||||
this.uploadProgress = 0;
|
||||
swal('Oops!', err.response.data.error, 'error');
|
||||
} else {
|
||||
this.isUploading = false;
|
||||
this.uploadProgress = 0;
|
||||
swal('Oops!', 'An error occured while processing your request, please try again later', 'error');
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
shortTimestamp(ts) {
|
||||
return window.App.util.format.timeAgo(ts);
|
||||
},
|
||||
|
||||
readMore() {
|
||||
this.readMoreCursor = this.readMoreCursor + 200;
|
||||
},
|
||||
|
||||
likeComment(status, index, $event) {
|
||||
$event.target.blur();
|
||||
let l = status.favourited ? false : true;
|
||||
this.feed[index].favourited = l;
|
||||
status.favourited = l;
|
||||
axios.post('/api/v0/groups/like', {
|
||||
sid: status.id,
|
||||
gid: this.groupId
|
||||
});
|
||||
},
|
||||
|
||||
deleteComment(index) {
|
||||
if(window.confirm('Are you sure you want to delete this post?') == false) {
|
||||
return;
|
||||
}
|
||||
|
||||
axios.post('/api/v0/groups/status/delete', {
|
||||
gid: this.groupId,
|
||||
id: this.feed[index].id
|
||||
}).then(res => {
|
||||
this.feed.splice(index, 1);
|
||||
}).catch(err => {
|
||||
console.log(err.response);
|
||||
swal('Error', 'Something went wrong. Please try again later.', 'error');
|
||||
});
|
||||
},
|
||||
|
||||
uploadImage() {
|
||||
this.$refs.fileInput.click();
|
||||
},
|
||||
|
||||
handleImageUpload() {
|
||||
if(!this.$refs.fileInput.files.length) {
|
||||
return;
|
||||
}
|
||||
this.isUploading = true;
|
||||
let self = this;
|
||||
let data = new FormData();
|
||||
data.append('gid', this.groupId);
|
||||
data.append('sid', this.status.id);
|
||||
data.append('photo', this.$refs.fileInput.files[0]);
|
||||
|
||||
axios.post('/api/v0/groups/comment/photo', data, {
|
||||
onUploadProgress: function(progressEvent) {
|
||||
self.uploadProgress = Math.floor(progressEvent.loaded / progressEvent.total * 100);
|
||||
}
|
||||
})
|
||||
.then(res => {
|
||||
this.isUploading = false;
|
||||
this.uploadProgress = 0;
|
||||
this.feed.unshift(res.data);
|
||||
}).catch(err => {
|
||||
if(err.response.status == 422) {
|
||||
this.isUploading = false;
|
||||
this.uploadProgress = 0;
|
||||
swal('Oops!', err.response.data.error, 'error');
|
||||
} else {
|
||||
this.isUploading = false;
|
||||
this.uploadProgress = 0;
|
||||
swal('Oops!', 'An error occured while processing your request, please try again later', 'error');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
lightbox(status) {
|
||||
this.lightboxStatus = status.media_attachments[0];
|
||||
this.$refs.lightboxModal.show();
|
||||
},
|
||||
|
||||
hideLightbox() {
|
||||
this.lightboxStatus = null;
|
||||
this.$refs.lightboxModal.hide();
|
||||
},
|
||||
|
||||
blurhashWidth(status) {
|
||||
if(!status.media_attachments[0].meta) {
|
||||
return 25;
|
||||
}
|
||||
let aspect = status.media_attachments[0].meta.original.aspect;
|
||||
if(aspect == 1) {
|
||||
return 25;
|
||||
} else if(aspect > 1) {
|
||||
return 30;
|
||||
} else {
|
||||
return 20;
|
||||
}
|
||||
},
|
||||
|
||||
blurhashHeight(status) {
|
||||
if(!status.media_attachments[0].meta) {
|
||||
return 25;
|
||||
}
|
||||
let aspect = status.media_attachments[0].meta.original.aspect;
|
||||
if(aspect == 1) {
|
||||
return 25;
|
||||
} else if(aspect > 1) {
|
||||
return 20;
|
||||
} else {
|
||||
return 30;
|
||||
}
|
||||
},
|
||||
|
||||
getMediaSource(status) {
|
||||
let media = status.media_attachments[0];
|
||||
|
||||
if(media.preview_url.endsWith('storage/no-preview.png')) {
|
||||
return media.url;
|
||||
}
|
||||
|
||||
return media.preview_url;
|
||||
},
|
||||
|
||||
replyToChild(status, index) {
|
||||
if(this.replyChildId == status.id) {
|
||||
this.replyChildId = null;
|
||||
return;
|
||||
} else {
|
||||
this.childReplyContent = null;
|
||||
}
|
||||
|
||||
this.replyChildId = status.id;
|
||||
|
||||
if(!status.hasOwnProperty('replies_loaded') || !status.replies_loaded) {
|
||||
this.fetchChildReplies(status, index);
|
||||
} else {
|
||||
|
||||
}
|
||||
},
|
||||
|
||||
fetchChildReplies(status, index) {
|
||||
axios.get('/api/v0/groups/comments', {
|
||||
params: {
|
||||
gid: this.groupId,
|
||||
sid: status.id,
|
||||
cid: 1,
|
||||
limit: 3
|
||||
}
|
||||
}).then(res => {
|
||||
if(this.feed[index].hasOwnProperty('children')) {
|
||||
this.feed[index].children.feed.push(...res.data);
|
||||
this.feed[index].children.can_load_more = res.data.length == 3;
|
||||
} else {
|
||||
this.feed[index].children = {
|
||||
feed: res.data,
|
||||
can_load_more: res.data.length == 3
|
||||
}
|
||||
}
|
||||
this.feed[index].replies_loaded = true;
|
||||
}).catch(err => {
|
||||
})
|
||||
},
|
||||
|
||||
storeChildComment() {
|
||||
this.postingChildComment = true;
|
||||
|
||||
axios.post('/api/v0/groups/comment', {
|
||||
gid: this.groupId,
|
||||
sid: this.status.id,
|
||||
cid: this.replyChildId,
|
||||
content: this.childReplyContent
|
||||
})
|
||||
.then(res => {
|
||||
this.childReplyContent = null;
|
||||
this.postingChildComment = false;
|
||||
// this.feed.unshift(res.data);
|
||||
}).catch(err => {
|
||||
if(err.response.status == 422) {
|
||||
// this.isUploading = false;
|
||||
// this.uploadProgress = 0;
|
||||
swal('Oops!', err.response.data.error, 'error');
|
||||
} else {
|
||||
// this.isUploading = false;
|
||||
// this.uploadProgress = 0;
|
||||
swal('Oops!', 'An error occured while processing your request, please try again later', 'error');
|
||||
}
|
||||
});
|
||||
|
||||
console.log(this.replyChildId);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.comment-post-component {
|
||||
|
||||
}
|
||||
</style>
|
692
resources/assets/components/groups/partials/ContextMenu.vue
Normal file
692
resources/assets/components/groups/partials/ContextMenu.vue
Normal file
|
@ -0,0 +1,692 @@
|
|||
<template>
|
||||
<div class="context-menu-component modal-stack">
|
||||
<b-modal ref="ctxModal"
|
||||
id="ctx-modal"
|
||||
hide-header
|
||||
hide-footer
|
||||
centered
|
||||
rounded
|
||||
size="sm"
|
||||
body-class="list-group-flush p-0 rounded">
|
||||
<div class="list-group text-center">
|
||||
<!-- <div v-if="status && status.account.id != profile.id && ctxMenuRelationship && ctxMenuRelationship.following" class="list-group-item rounded cursor-pointer font-weight-bold text-danger" @click="ctxMenuUnfollow()">Unfollow</div>
|
||||
<div v-if="status && status.account.id != profile.id && ctxMenuRelationship && !ctxMenuRelationship.following" class="list-group-item rounded cursor-pointer font-weight-bold text-primary" @click="ctxMenuFollow()">Follow</div> -->
|
||||
<div v-if="status.visibility !== 'archived'" class="list-group-item rounded cursor-pointer" @click="ctxMenuGoToPost()">View Post</div>
|
||||
<!-- <div v-if="status.visibility !== 'archived'" class="list-group-item rounded cursor-pointer" @click="ctxMenuGoToProfile()">View Profile</div> -->
|
||||
<!-- <div v-if="status && status.local == true && !status.in_reply_to_id" class="list-group-item rounded cursor-pointer" @click="ctxMenuEmbed()">Embed</div> -->
|
||||
<div class="list-group-item rounded cursor-pointer" @click="ctxMenuCopyLink()">Copy Link</div>
|
||||
<!-- <div v-if="status.visibility !== 'archived'" class="list-group-item rounded cursor-pointer" @click="ctxMenuShare()">Share</div> -->
|
||||
<!-- <div v-if="status && profile && profile.is_admin == true && status.visibility !== 'archived'" class="list-group-item rounded cursor-pointer" @click="ctxModMenuShow()">Moderation Tools</div> -->
|
||||
<div v-if="status && status.account.id != profile.id" class="list-group-item rounded cursor-pointer text-danger" @click="ctxMenuReportPost()">Report</div>
|
||||
<div v-if="status && (profile.is_admin || profile.id == status.account.id) && status.visibility !== 'archived'" class="list-group-item rounded cursor-pointer text-danger" @click="deletePost(status)">Delete</div>
|
||||
<div class="list-group-item rounded cursor-pointer text-lighter" @click="closeCtxMenu()">Cancel</div>
|
||||
</div>
|
||||
</b-modal>
|
||||
<b-modal ref="ctxModModal"
|
||||
id="ctx-mod-modal"
|
||||
hide-header
|
||||
hide-footer
|
||||
centered
|
||||
rounded
|
||||
size="sm"
|
||||
body-class="list-group-flush p-0 rounded">
|
||||
<div class="list-group text-center">
|
||||
<p class="py-2 px-3 mb-0">
|
||||
<div class="text-center font-weight-bold text-danger">Moderation Tools</div>
|
||||
<div class="small text-center text-muted">Select one of the following options</div>
|
||||
</p>
|
||||
<div class="list-group-item rounded cursor-pointer" @click="moderatePost(status, 'unlist')">Unlist from Timelines</div>
|
||||
<div v-if="status.sensitive" class="list-group-item rounded cursor-pointer" @click="moderatePost(status, 'remcw')">Remove Content Warning</div>
|
||||
<div v-else class="list-group-item rounded cursor-pointer" @click="moderatePost(status, 'addcw')">Add Content Warning</div>
|
||||
<div class="list-group-item rounded cursor-pointer" @click="moderatePost(status, 'spammer')">
|
||||
Mark as Spammer<br />
|
||||
<span class="small">Unlist + CW existing and future posts</span>
|
||||
</div>
|
||||
<!-- <div class="list-group-item rounded cursor-pointer" @click="ctxModOtherMenuShow()">Other</div> -->
|
||||
<div class="list-group-item rounded cursor-pointer text-lighter" @click="ctxModMenuClose()">Cancel</div>
|
||||
</div>
|
||||
</b-modal>
|
||||
<b-modal ref="ctxModOtherModal"
|
||||
id="ctx-mod-other-modal"
|
||||
hide-header
|
||||
hide-footer
|
||||
centered
|
||||
rounded
|
||||
size="sm"
|
||||
body-class="list-group-flush p-0 rounded">
|
||||
<div class="list-group text-center">
|
||||
<p class="py-2 px-3 mb-0">
|
||||
<div class="text-center font-weight-bold text-danger">Moderation Tools</div>
|
||||
<div class="small text-center text-muted">Select one of the following options</div>
|
||||
</p>
|
||||
<div class="list-group-item rounded cursor-pointer font-weight-bold" @click="confirmModal()">Unlist Posts</div>
|
||||
<div class="list-group-item rounded cursor-pointer font-weight-bold" @click="confirmModal()">Moderation Log</div>
|
||||
<div class="list-group-item rounded cursor-pointer text-lighter" @click="ctxModOtherMenuClose()">Cancel</div>
|
||||
</div>
|
||||
</b-modal>
|
||||
<b-modal ref="ctxShareModal"
|
||||
id="ctx-share-modal"
|
||||
title="Share"
|
||||
hide-footer
|
||||
hide-header
|
||||
centered
|
||||
rounded
|
||||
size="sm"
|
||||
body-class="list-group-flush p-0 rounded text-center">
|
||||
<div class="list-group-item rounded cursor-pointer" @click="ctxMenuCopyLink()">Copy Link</div>
|
||||
<div class="list-group-item rounded cursor-pointer text-lighter" @click="closeCtxShareMenu()">Cancel</div>
|
||||
</b-modal>
|
||||
<b-modal ref="ctxEmbedModal"
|
||||
id="ctx-embed-modal"
|
||||
hide-header
|
||||
hide-footer
|
||||
centered
|
||||
rounded
|
||||
size="md"
|
||||
body-class="p-2 rounded">
|
||||
<div>
|
||||
<div class="form-group">
|
||||
<textarea class="form-control disabled text-monospace" rows="8" style="overflow-y:hidden;border: 1px solid #efefef; font-size: 12px; line-height: 18px; margin: 0 0 7px;resize:none;" v-model="ctxEmbedPayload" disabled=""></textarea>
|
||||
</div>
|
||||
<div class="form-group pl-2 d-flex justify-content-center">
|
||||
<div class="form-check mr-3">
|
||||
<input class="form-check-input" type="checkbox" v-model="ctxEmbedShowCaption" :disabled="ctxEmbedCompactMode == true">
|
||||
<label class="form-check-label font-weight-light">
|
||||
Show Caption
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check mr-3">
|
||||
<input class="form-check-input" type="checkbox" v-model="ctxEmbedShowLikes" :disabled="ctxEmbedCompactMode == true">
|
||||
<label class="form-check-label font-weight-light">
|
||||
Show Likes
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" v-model="ctxEmbedCompactMode">
|
||||
<label class="form-check-label font-weight-light">
|
||||
Compact Mode
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<button :class="copiedEmbed ? 'btn btn-primary btn-block btn-sm py-1 font-weight-bold disabed': 'btn btn-primary btn-block btn-sm py-1 font-weight-bold'" @click="ctxCopyEmbed" :disabled="copiedEmbed">{{copiedEmbed ? 'Embed Code Copied!' : 'Copy Embed Code'}}</button>
|
||||
<p class="mb-0 px-2 small text-muted">By using this embed, you agree to our <a href="/site/terms">Terms of Use</a></p>
|
||||
</div>
|
||||
</b-modal>
|
||||
<b-modal ref="ctxReport"
|
||||
id="ctx-report"
|
||||
hide-header
|
||||
hide-footer
|
||||
centered
|
||||
rounded
|
||||
size="sm"
|
||||
body-class="list-group-flush p-0 rounded">
|
||||
<p class="py-2 px-3 mb-0">
|
||||
<div class="text-center font-weight-bold text-danger">Report</div>
|
||||
<div class="small text-center text-muted">Select one of the following options</div>
|
||||
</p>
|
||||
<div class="list-group text-center">
|
||||
<div class="list-group-item rounded cursor-pointer font-weight-bold" @click="sendReport('spam')">Spam</div>
|
||||
<div class="list-group-item rounded cursor-pointer font-weight-bold" @click="sendReport('sensitive')">Sensitive Content</div>
|
||||
<div class="list-group-item rounded cursor-pointer font-weight-bold" @click="sendReport('abusive')">Abusive or Harmful</div>
|
||||
<div class="list-group-item rounded cursor-pointer font-weight-bold" @click="openCtxReportOtherMenu()">Other</div>
|
||||
<!-- <div class="list-group-item rounded cursor-pointer" @click="ctxReportMenuGoBack()">Go Back</div> -->
|
||||
<div class="list-group-item rounded cursor-pointer text-lighter" @click="ctxReportMenuGoBack()">Cancel</div>
|
||||
</div>
|
||||
</b-modal>
|
||||
<b-modal ref="ctxReportOther"
|
||||
id="ctx-report-other"
|
||||
hide-header
|
||||
hide-footer
|
||||
centered
|
||||
rounded
|
||||
size="sm"
|
||||
body-class="list-group-flush p-0 rounded">
|
||||
<p class="py-2 px-3 mb-0">
|
||||
<div class="text-center font-weight-bold text-danger">Report</div>
|
||||
<div class="small text-center text-muted">Select one of the following options</div>
|
||||
</p>
|
||||
<div class="list-group text-center">
|
||||
<div class="list-group-item rounded cursor-pointer font-weight-bold" @click="sendReport('underage')">Underage Account</div>
|
||||
<div class="list-group-item rounded cursor-pointer font-weight-bold" @click="sendReport('copyright')">Copyright Infringement</div>
|
||||
<div class="list-group-item rounded cursor-pointer font-weight-bold" @click="sendReport('impersonation')">Impersonation</div>
|
||||
<div class="list-group-item rounded cursor-pointer font-weight-bold" @click="sendReport('scam')">Scam or Fraud</div>
|
||||
<!-- <div class="list-group-item rounded cursor-pointer font-weight-bold" @click="sendReport('terrorism')">Terrorism Related</div> -->
|
||||
<!-- <div class="list-group-item rounded cursor-pointer font-weight-bold" @click="sendReport('other')">Other or Not listed</div> -->
|
||||
<!-- <div class="list-group-item rounded cursor-pointer" @click="ctxReportOtherMenuGoBack()">Go Back</div> -->
|
||||
<div class="list-group-item rounded cursor-pointer text-lighter" @click="ctxReportOtherMenuGoBack()">Cancel</div>
|
||||
</div>
|
||||
</b-modal>
|
||||
<b-modal ref="ctxConfirm"
|
||||
id="ctx-confirm"
|
||||
hide-header
|
||||
hide-footer
|
||||
centered
|
||||
rounded
|
||||
size="sm"
|
||||
body-class="list-group-flush p-0 rounded">
|
||||
<div class="d-flex align-items-center justify-content-center py-3">
|
||||
<div>{{ this.confirmModalTitle }}</div>
|
||||
</div>
|
||||
<div class="d-flex border-top btn-group btn-group-block rounded-0" role="group">
|
||||
<button type="button" class="btn btn-outline-lighter border-left-0 border-top-0 border-bottom-0 border-right py-2" style="color: rgb(0,122,255) !important;" @click.prevent="confirmModalCancel()">Cancel</button>
|
||||
<button type="button" class="btn btn-outline-lighter border-0" style="color: rgb(0,122,255) !important;" @click.prevent="confirmModalConfirm()">Confirm</button>
|
||||
</div>
|
||||
</b-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script type="text/javascript">
|
||||
export default {
|
||||
props: {
|
||||
status: {
|
||||
type: Object
|
||||
},
|
||||
|
||||
profile: {
|
||||
type: Object
|
||||
},
|
||||
|
||||
type: {
|
||||
type: String,
|
||||
default: 'status',
|
||||
validator: (val) => ['status', 'comment', 'profile'].includes(val)
|
||||
},
|
||||
|
||||
groupId: {
|
||||
type: String
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
ctxMenuStatus: false,
|
||||
ctxMenuRelationship: false,
|
||||
ctxEmbedPayload: false,
|
||||
copiedEmbed: false,
|
||||
replySending: false,
|
||||
ctxEmbedShowCaption: true,
|
||||
ctxEmbedShowLikes: false,
|
||||
ctxEmbedCompactMode: false,
|
||||
confirmModalTitle: 'Are you sure?',
|
||||
confirmModalIdentifer: null,
|
||||
confirmModalType: false,
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
open() {
|
||||
this.ctxMenu();
|
||||
},
|
||||
|
||||
ctxMenu() {
|
||||
this.ctxMenuStatus = this.status;
|
||||
this.ctxEmbedPayload = window.App.util.embed.post(this.status.url);
|
||||
if(this.status.account.id == this.profile.id) {
|
||||
this.ctxMenuRelationship = false;
|
||||
this.$refs.ctxModal.show();
|
||||
} else {
|
||||
axios.get('/api/pixelfed/v1/accounts/relationships', {
|
||||
params: {
|
||||
'id[]': this.status.account.id
|
||||
}
|
||||
}).then(res => {
|
||||
this.ctxMenuRelationship = res.data[0];
|
||||
this.$refs.ctxModal.show();
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
closeCtxMenu() {
|
||||
this.copiedEmbed = false;
|
||||
this.ctxMenuStatus = false;
|
||||
this.ctxMenuRelationship = false;
|
||||
this.$refs.ctxModal.hide();
|
||||
this.$refs.ctxReport.hide();
|
||||
this.$refs.ctxReportOther.hide();
|
||||
this.closeModals();
|
||||
},
|
||||
|
||||
ctxMenuCopyLink() {
|
||||
let status = this.ctxMenuStatus;
|
||||
navigator.clipboard.writeText(status.url);
|
||||
this.closeModals();
|
||||
return;
|
||||
},
|
||||
|
||||
ctxMenuGoToPost() {
|
||||
let status = this.ctxMenuStatus;
|
||||
window.location.href = this.statusUrl(status);
|
||||
this.closeCtxMenu();
|
||||
return;
|
||||
},
|
||||
|
||||
ctxMenuGoToProfile() {
|
||||
let status = this.ctxMenuStatus;
|
||||
window.location.href = this.profileUrl(status);
|
||||
this.closeCtxMenu();
|
||||
return;
|
||||
},
|
||||
|
||||
ctxMenuFollow() {
|
||||
let id = this.ctxMenuStatus.account.id;
|
||||
axios.post('/i/follow', {
|
||||
item: id
|
||||
}).then(res => {
|
||||
let username = this.ctxMenuStatus.account.acct;
|
||||
this.closeCtxMenu();
|
||||
setTimeout(function() {
|
||||
swal('Follow successful!', 'You are now following ' + username, 'success');
|
||||
}, 500);
|
||||
});
|
||||
},
|
||||
|
||||
ctxMenuUnfollow() {
|
||||
let id = this.ctxMenuStatus.account.id;
|
||||
axios.post('/i/follow', {
|
||||
item: id
|
||||
}).then(res => {
|
||||
let username = this.ctxMenuStatus.account.acct;
|
||||
if(this.scope == 'home') {
|
||||
this.feed = this.feed.filter(s => {
|
||||
return s.account.id != this.ctxMenuStatus.account.id;
|
||||
});
|
||||
}
|
||||
this.closeCtxMenu();
|
||||
setTimeout(function() {
|
||||
swal('Unfollow successful!', 'You are no longer following ' + username, 'success');
|
||||
}, 500);
|
||||
});
|
||||
},
|
||||
|
||||
ctxMenuReportPost() {
|
||||
this.$refs.ctxModal.hide();
|
||||
this.$refs.ctxReport.show();
|
||||
return;
|
||||
},
|
||||
|
||||
ctxMenuEmbed() {
|
||||
this.closeModals();
|
||||
this.$refs.ctxEmbedModal.show();
|
||||
},
|
||||
|
||||
ctxMenuShare() {
|
||||
this.$refs.ctxModal.hide();
|
||||
this.$refs.ctxShareModal.show();
|
||||
},
|
||||
|
||||
closeCtxShareMenu() {
|
||||
this.$refs.ctxShareModal.hide();
|
||||
this.$refs.ctxModal.show();
|
||||
},
|
||||
|
||||
ctxCopyEmbed() {
|
||||
navigator.clipboard.writeText(this.ctxEmbedPayload);
|
||||
this.ctxEmbedShowCaption = true;
|
||||
this.ctxEmbedShowLikes = false;
|
||||
this.ctxEmbedCompactMode = false;
|
||||
this.$refs.ctxEmbedModal.hide();
|
||||
},
|
||||
|
||||
ctxModMenuShow() {
|
||||
this.$refs.ctxModal.hide();
|
||||
this.$refs.ctxModModal.show();
|
||||
},
|
||||
|
||||
ctxModOtherMenuShow() {
|
||||
this.$refs.ctxModal.hide();
|
||||
this.$refs.ctxModModal.hide();
|
||||
this.$refs.ctxModOtherModal.show();
|
||||
},
|
||||
|
||||
ctxModMenu() {
|
||||
this.$refs.ctxModal.hide();
|
||||
},
|
||||
|
||||
ctxModMenuClose() {
|
||||
this.closeModals();
|
||||
},
|
||||
|
||||
ctxModOtherMenuClose() {
|
||||
this.closeModals();
|
||||
this.$refs.ctxModModal.show();
|
||||
},
|
||||
|
||||
formatCount(count) {
|
||||
return App.util.format.count(count);
|
||||
},
|
||||
|
||||
openCtxReportOtherMenu() {
|
||||
let s = this.ctxMenuStatus;
|
||||
this.closeCtxMenu();
|
||||
this.ctxMenuStatus = s;
|
||||
this.$refs.ctxReportOther.show();
|
||||
},
|
||||
|
||||
ctxReportMenuGoBack() {
|
||||
this.$refs.ctxReportOther.hide();
|
||||
this.$refs.ctxReport.hide();
|
||||
this.$refs.ctxModal.show();
|
||||
},
|
||||
|
||||
ctxReportOtherMenuGoBack() {
|
||||
this.$refs.ctxReportOther.hide();
|
||||
this.$refs.ctxModal.hide();
|
||||
this.$refs.ctxReport.show();
|
||||
},
|
||||
|
||||
sendReport(type) {
|
||||
let id = this.ctxMenuStatus.id;
|
||||
|
||||
swal({
|
||||
'title': 'Confirm Report',
|
||||
'text': 'Are you sure you want to report this post?',
|
||||
'icon': 'warning',
|
||||
'buttons': true,
|
||||
'dangerMode': true
|
||||
}).then((res) => {
|
||||
if(res) {
|
||||
axios.post(`/api/v0/groups/${this.groupId}/report/create`, {
|
||||
'type': type,
|
||||
'id': id,
|
||||
}).then(res => {
|
||||
this.closeCtxMenu();
|
||||
swal('Report Sent!', 'We have successfully received your report.', 'success');
|
||||
}).catch(err => {
|
||||
if(err.response.status == 422) {
|
||||
swal('Oops!', err.response.data.error, 'error');
|
||||
} else {
|
||||
swal('Oops!', 'There was an issue reporting this post.', 'error');
|
||||
}
|
||||
})
|
||||
} else {
|
||||
this.closeCtxMenu();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
closeModals() {
|
||||
this.$refs.ctxModal.hide();
|
||||
this.$refs.ctxModModal.hide();
|
||||
this.$refs.ctxModOtherModal.hide();
|
||||
this.$refs.ctxShareModal.hide();
|
||||
this.$refs.ctxEmbedModal.hide();
|
||||
this.$refs.ctxReport.hide();
|
||||
this.$refs.ctxReportOther.hide();
|
||||
this.$refs.ctxConfirm.hide();
|
||||
},
|
||||
|
||||
openCtxStatusModal() {
|
||||
this.closeModals();
|
||||
this.$refs.ctxStatusModal.show();
|
||||
},
|
||||
|
||||
openConfirmModal() {
|
||||
this.closeModals();
|
||||
this.$refs.ctxConfirm.show();
|
||||
},
|
||||
|
||||
closeConfirmModal() {
|
||||
this.closeModals();
|
||||
this.confirmModalTitle = 'Are you sure?';
|
||||
this.confirmModalType = false;
|
||||
this.confirmModalIdentifer = null;
|
||||
},
|
||||
|
||||
confirmModalConfirm() {
|
||||
switch(this.confirmModalType) {
|
||||
case 'post.delete':
|
||||
axios.post('/i/delete', {
|
||||
type: 'status',
|
||||
item: this.confirmModalIdentifer
|
||||
}).then(res => {
|
||||
this.feed = this.feed.filter(s => {
|
||||
return s.id != this.confirmModalIdentifer;
|
||||
});
|
||||
this.closeConfirmModal();
|
||||
}).catch(err => {
|
||||
this.closeConfirmModal();
|
||||
swal('Error', 'Something went wrong. Please try again later.', 'error');
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
this.closeConfirmModal();
|
||||
},
|
||||
|
||||
confirmModalCancel() {
|
||||
this.closeConfirmModal();
|
||||
},
|
||||
|
||||
moderatePost(status, action, $event) {
|
||||
let username = status.account.username;
|
||||
let pid = status.id;
|
||||
let msg = '';
|
||||
let self = this;
|
||||
switch(action) {
|
||||
case 'addcw':
|
||||
msg = 'Are you sure you want to add a content warning to this post?';
|
||||
swal({
|
||||
title: 'Confirm',
|
||||
text: msg,
|
||||
icon: 'warning',
|
||||
buttons: true,
|
||||
dangerMode: true
|
||||
}).then(res => {
|
||||
if(res) {
|
||||
axios.post('/api/v2/moderator/action', {
|
||||
action: action,
|
||||
item_id: status.id,
|
||||
item_type: 'status'
|
||||
}).then(res => {
|
||||
swal('Success', 'Successfully added content warning', 'success');
|
||||
status.sensitive = true;
|
||||
self.closeModals();
|
||||
self.ctxModMenuClose();
|
||||
}).catch(err => {
|
||||
swal(
|
||||
'Error',
|
||||
'Something went wrong, please try again later.',
|
||||
'error'
|
||||
);
|
||||
self.closeModals();
|
||||
self.ctxModMenuClose();
|
||||
});
|
||||
}
|
||||
});
|
||||
break;
|
||||
|
||||
case 'remcw':
|
||||
msg = 'Are you sure you want to remove the content warning on this post?';
|
||||
swal({
|
||||
title: 'Confirm',
|
||||
text: msg,
|
||||
icon: 'warning',
|
||||
buttons: true,
|
||||
dangerMode: true
|
||||
}).then(res => {
|
||||
if(res) {
|
||||
axios.post('/api/v2/moderator/action', {
|
||||
action: action,
|
||||
item_id: status.id,
|
||||
item_type: 'status'
|
||||
}).then(res => {
|
||||
swal('Success', 'Successfully added content warning', 'success');
|
||||
status.sensitive = false;
|
||||
self.closeModals();
|
||||
self.ctxModMenuClose();
|
||||
}).catch(err => {
|
||||
swal(
|
||||
'Error',
|
||||
'Something went wrong, please try again later.',
|
||||
'error'
|
||||
);
|
||||
self.closeModals();
|
||||
self.ctxModMenuClose();
|
||||
});
|
||||
}
|
||||
});
|
||||
break;
|
||||
|
||||
case 'unlist':
|
||||
msg = 'Are you sure you want to unlist this post?';
|
||||
swal({
|
||||
title: 'Confirm',
|
||||
text: msg,
|
||||
icon: 'warning',
|
||||
buttons: true,
|
||||
dangerMode: true
|
||||
}).then(res => {
|
||||
if(res) {
|
||||
axios.post('/api/v2/moderator/action', {
|
||||
action: action,
|
||||
item_id: status.id,
|
||||
item_type: 'status'
|
||||
}).then(res => {
|
||||
this.feed = this.feed.filter(f => {
|
||||
return f.id != status.id;
|
||||
});
|
||||
swal('Success', 'Successfully unlisted post', 'success');
|
||||
self.closeModals();
|
||||
self.ctxModMenuClose();
|
||||
}).catch(err => {
|
||||
self.closeModals();
|
||||
self.ctxModMenuClose();
|
||||
swal(
|
||||
'Error',
|
||||
'Something went wrong, please try again later.',
|
||||
'error'
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
break;
|
||||
|
||||
case 'spammer':
|
||||
msg = 'Are you sure you want to mark this user as a spammer? All existing and future posts will be unlisted on timelines and a content warning will be applied.';
|
||||
swal({
|
||||
title: 'Confirm',
|
||||
text: msg,
|
||||
icon: 'warning',
|
||||
buttons: true,
|
||||
dangerMode: true
|
||||
}).then(res => {
|
||||
if(res) {
|
||||
axios.post('/api/v2/moderator/action', {
|
||||
action: action,
|
||||
item_id: status.id,
|
||||
item_type: 'status'
|
||||
}).then(res => {
|
||||
swal('Success', 'Successfully marked account as spammer', 'success');
|
||||
self.closeModals();
|
||||
self.ctxModMenuClose();
|
||||
}).catch(err => {
|
||||
self.closeModals();
|
||||
self.ctxModMenuClose();
|
||||
swal(
|
||||
'Error',
|
||||
'Something went wrong, please try again later.',
|
||||
'error'
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
shareStatus(status, $event) {
|
||||
if($('body').hasClass('loggedIn') == false) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.closeModals();
|
||||
|
||||
axios.post('/i/share', {
|
||||
item: status.id
|
||||
}).then(res => {
|
||||
status.reblogs_count = res.data.count;
|
||||
status.reblogged = !status.reblogged;
|
||||
if(status.reblogged) {
|
||||
swal('Success', 'You shared this post', 'success');
|
||||
} else {
|
||||
swal('Success', 'You unshared this post', 'success');
|
||||
}
|
||||
}).catch(err => {
|
||||
swal('Error', 'Something went wrong, please try again later.', 'error');
|
||||
});
|
||||
},
|
||||
|
||||
statusUrl(status) {
|
||||
if(status.local == true) {
|
||||
return status.url;
|
||||
}
|
||||
|
||||
return '/i/web/post/_/' + status.account.id + '/' + status.id;
|
||||
},
|
||||
|
||||
profileUrl(status) {
|
||||
if(status.local == true) {
|
||||
return status.account.url;
|
||||
}
|
||||
|
||||
return '/i/web/profile/_/' + status.account.id;
|
||||
},
|
||||
|
||||
deletePost(status) {
|
||||
if($('body').hasClass('loggedIn') == false || this.ownerOrAdmin(status) == false) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(window.confirm('Are you sure you want to delete this post?') == false) {
|
||||
return;
|
||||
}
|
||||
|
||||
axios.post('/i/delete', {
|
||||
type: 'status',
|
||||
item: status.id
|
||||
}).then(res => {
|
||||
this.$emit('status-delete', status.id);
|
||||
this.closeModals();
|
||||
}).catch(err => {
|
||||
swal('Error', 'Something went wrong. Please try again later.', 'error');
|
||||
});
|
||||
},
|
||||
|
||||
owner(status) {
|
||||
return this.profile.id === status.account.id;
|
||||
},
|
||||
|
||||
admin() {
|
||||
return this.profile.is_admin == true;
|
||||
},
|
||||
|
||||
ownerOrAdmin(status) {
|
||||
return this.owner(status) || this.admin();
|
||||
},
|
||||
|
||||
archivePost(status) {
|
||||
if(window.confirm('Are you sure you want to archive this post?') == false) {
|
||||
return;
|
||||
}
|
||||
|
||||
axios.post('/api/pixelfed/v2/status/' + status.id + '/archive')
|
||||
.then(res => {
|
||||
this.$emit('status-delete', status.id);
|
||||
this.closeModals();
|
||||
});
|
||||
},
|
||||
|
||||
unarchivePost(status) {
|
||||
if(window.confirm('Are you sure you want to unarchive this post?') == false) {
|
||||
return;
|
||||
}
|
||||
|
||||
axios.post('/api/pixelfed/v2/status/' + status.id + '/unarchive')
|
||||
.then(res => {
|
||||
this.closeModals();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,59 @@
|
|||
<template>
|
||||
<div class="form-group row">
|
||||
<div class="col-sm-3">
|
||||
<label class="col-form-label text-left">{{ label }}</label>
|
||||
</div>
|
||||
<div class="col-sm-9">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" v-model="value">
|
||||
<label
|
||||
class="form-check-label ml-1"
|
||||
:class="[ strongText ? 'font-weight-bold text-capitalize text-dark' : 'small text-muted' ]"
|
||||
>
|
||||
{{ inputText }}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="helpText"
|
||||
class="help-text small text-muted d-flex flex-row justify-content-between gap-3">
|
||||
<div v-if="helpText">{{ helpText }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
label: {
|
||||
type: String
|
||||
},
|
||||
inputText: {
|
||||
type: String
|
||||
},
|
||||
val: {
|
||||
type: String
|
||||
},
|
||||
helpText: {
|
||||
type: String
|
||||
},
|
||||
strongText: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
value: this.val
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
value: function(newVal, oldVal) {
|
||||
this.$emit('update', newVal);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,70 @@
|
|||
<template>
|
||||
<div class="form-group row">
|
||||
<div class="col-sm-3">
|
||||
<label class="col-form-label text-left">{{ label }}</label>
|
||||
</div>
|
||||
<div class="col-sm-9">
|
||||
<select class="custom-select" v-model="value">
|
||||
<option value="" selected="" disabled="">{{ placeholder }}</option>
|
||||
<option v-for="c in categories" :value="c.value">{{ c.key }}</option>
|
||||
</select>
|
||||
|
||||
<div
|
||||
v-if="helpText || hasLimit"
|
||||
class="help-text small text-muted d-flex flex-row justify-content-between gap-3">
|
||||
<div v-if="helpText">{{ helpText }}</div>
|
||||
<div
|
||||
v-if="hasLimit"
|
||||
class="font-weight-bold text-dark">
|
||||
{{ value ? value.length : 0 }}/{{ maxLimit }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
label: {
|
||||
type: String
|
||||
},
|
||||
placeholder: {
|
||||
type: String
|
||||
},
|
||||
categories: {
|
||||
type: Array
|
||||
},
|
||||
val: {
|
||||
type: String
|
||||
},
|
||||
helpText: {
|
||||
type: String
|
||||
},
|
||||
hasLimit: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
maxLimit: {
|
||||
type: Number,
|
||||
default: 40
|
||||
},
|
||||
largeInput: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
value: this.val ? this.val : ""
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
value: function(newVal, oldVal) {
|
||||
this.$emit('update', newVal);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,86 @@
|
|||
<template>
|
||||
<div class="form-group row">
|
||||
<div class="col-sm-3">
|
||||
<label class="col-form-label text-left">{{ label }}</label>
|
||||
</div>
|
||||
<div class="col-sm-9">
|
||||
<textarea
|
||||
v-if="hasLimit"
|
||||
type="text"
|
||||
class="form-control"
|
||||
:class="{ 'form-control-lg': largeInput }"
|
||||
style="resize:none;"
|
||||
:placeholder="placeholder"
|
||||
:maxlength="maxLimit"
|
||||
:rows="rows"
|
||||
v-model="value" />
|
||||
<textarea
|
||||
v-else
|
||||
type="text"
|
||||
class="form-control"
|
||||
:class="{ 'form-control-lg': largeInput }"
|
||||
style="resize:none;"
|
||||
:placeholder="placeholder"
|
||||
:rows="rows"
|
||||
v-model="value" />
|
||||
|
||||
<div
|
||||
v-if="helpText || hasLimit"
|
||||
class="help-text small text-muted d-flex flex-row justify-content-between gap-3">
|
||||
<div v-if="helpText">{{ helpText }}</div>
|
||||
<div
|
||||
v-if="hasLimit"
|
||||
class="font-weight-bold text-dark">
|
||||
{{ value ? value.length : 0 }}/{{ maxLimit }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
label: {
|
||||
type: String
|
||||
},
|
||||
placeholder: {
|
||||
type: String
|
||||
},
|
||||
val: {
|
||||
type: String
|
||||
},
|
||||
helpText: {
|
||||
type: String
|
||||
},
|
||||
hasLimit: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
maxLimit: {
|
||||
type: Number,
|
||||
default: 40
|
||||
},
|
||||
largeInput: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
rows: {
|
||||
type: Number,
|
||||
default: 4
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
value: this.val
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
value: function(newVal, oldVal) {
|
||||
this.$emit('update', newVal);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,78 @@
|
|||
<template>
|
||||
<div class="form-group row">
|
||||
<div class="col-sm-3">
|
||||
<label class="col-form-label text-left">{{ label }}</label>
|
||||
</div>
|
||||
<div class="col-sm-9">
|
||||
<input
|
||||
v-if="hasLimit"
|
||||
type="text"
|
||||
class="form-control"
|
||||
:class="{ 'form-control-lg': largeInput }"
|
||||
:placeholder="placeholder"
|
||||
:maxlength="maxLimit"
|
||||
v-model="value">
|
||||
<input
|
||||
v-else
|
||||
type="text"
|
||||
class="form-control"
|
||||
:class="{ 'form-control-lg': largeInput }"
|
||||
:placeholder="placeholder"
|
||||
v-model="value">
|
||||
|
||||
<div
|
||||
v-if="helpText || hasLimit"
|
||||
class="help-text small text-muted d-flex flex-row justify-content-between gap-3">
|
||||
<div v-if="helpText">{{ helpText }}</div>
|
||||
<div
|
||||
v-if="hasLimit"
|
||||
class="font-weight-bold text-dark">
|
||||
{{ value ? value.length : 0 }}/{{ maxLimit }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
label: {
|
||||
type: String
|
||||
},
|
||||
placeholder: {
|
||||
type: String
|
||||
},
|
||||
val: {
|
||||
type: String
|
||||
},
|
||||
helpText: {
|
||||
type: String
|
||||
},
|
||||
hasLimit: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
maxLimit: {
|
||||
type: Number,
|
||||
default: 40
|
||||
},
|
||||
largeInput: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
value: this.val
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
value: function(newVal, oldVal) {
|
||||
this.$emit('update', newVal);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
134
resources/assets/components/groups/partials/GroupAbout.vue
Normal file
134
resources/assets/components/groups/partials/GroupAbout.vue
Normal file
|
@ -0,0 +1,134 @@
|
|||
<template>
|
||||
<div class="group-about-component">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-12 col-md-7">
|
||||
<div class="card shadow-none border mt-3 rounded-lg">
|
||||
<div class="card-header bg-white">
|
||||
<h5 class="mb-0">About This Group</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p v-if="group.description && group.description.length > 1" class="description" v-html="group.description"></p>
|
||||
<p v-else class="description">This group does not have a description.</p>
|
||||
<p class="mb-0 font-weight-light text-lighter">Created: {{ timestampFormat(group.created_at) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-5">
|
||||
<div class="card card-body mt-3 shadow-none border rounded-lg">
|
||||
<div v-if="group.membership == 'all'" class="fact">
|
||||
<div class="fact-icon">
|
||||
<i class="fal fa-globe fa-lg"></i>
|
||||
</div>
|
||||
<div class="fact-body">
|
||||
<p class="fact-title">Public</p>
|
||||
<p class="fact-subtitle">Anyone can see who's in the group and what they post.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="group.membership == 'private'" class="fact">
|
||||
<div class="fact-icon">
|
||||
<i class="fal fa-lock fa-lg"></i>
|
||||
</div>
|
||||
<div class="fact-body">
|
||||
<p class="fact-title">Private</p>
|
||||
<p class="fact-subtitle">Only members can see who's in the group and what they post.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="fact">
|
||||
<div class="fact-icon">
|
||||
<i class="fal fa-eye fa-lg"></i>
|
||||
</div>
|
||||
<div class="fact-body">
|
||||
<p class="fact-title">Visible</p>
|
||||
<p class="fact-subtitle">Anyone can find this group.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="fact">
|
||||
<div class="fact-icon">
|
||||
<i class="fal fa-map-marker-alt fa-lg"></i>
|
||||
</div>
|
||||
<div class="fact-body">
|
||||
<p class="fact-title">Fediverse</p>
|
||||
<p class="fact-subtitle">This group has not specified a location.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="fact mb-0">
|
||||
<div class="fact-icon">
|
||||
<i class="fal fa-users fa-lg"></i>
|
||||
</div>
|
||||
<div class="fact-body">
|
||||
<p class="fact-title"">General</p>
|
||||
<p class="fact-subtitle">This group has not specified a category.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script type="text/javascript">
|
||||
export default {
|
||||
props: {
|
||||
group: {
|
||||
type: Object
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
timestampFormat(date, showTime = false) {
|
||||
let ts = new Date(date);
|
||||
return showTime ? ts.toDateString() + ' · ' + ts.toLocaleTimeString() : ts.toDateString();
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.group-about-component {
|
||||
margin-bottom: 50vh;
|
||||
|
||||
.title {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: 15px;
|
||||
font-weight:400;
|
||||
color: #6c757d;
|
||||
margin-bottom: 30px;
|
||||
white-space: break-spaces;
|
||||
}
|
||||
|
||||
.fact {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 1.5rem;
|
||||
|
||||
&-body {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
&-icon {
|
||||
width: 50px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&-title {
|
||||
font-size: 17px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
&-subtitle {
|
||||
font-size: 14px;
|
||||
margin-bottom: 0;
|
||||
color: #6c757d;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
174
resources/assets/components/groups/partials/GroupCard.vue
Normal file
174
resources/assets/components/groups/partials/GroupCard.vue
Normal file
|
@ -0,0 +1,174 @@
|
|||
<template>
|
||||
<div class="col-12 col-md-6 col-xl-4 group-card">
|
||||
<div class="group-card-inner">
|
||||
<img
|
||||
v-if="group.metadata && group.metadata.hasOwnProperty('header')"
|
||||
:src="group.metadata.header.url"
|
||||
class="group-header-img" />
|
||||
|
||||
<div
|
||||
v-else
|
||||
class="group-header-img"
|
||||
:class="{ compact: compact }">
|
||||
<div
|
||||
class="bg-light d-flex align-items-center justify-content-center rounded"
|
||||
style="width: 100%; height:100%;">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="group-card-inner-copy">
|
||||
<p class="font-weight-bold mb-0 text-dark" style="font-size: 16px;">
|
||||
{{ truncate(group.name || 'Untitled Group', titleLength) }}
|
||||
</p>
|
||||
|
||||
<p class="text-muted mb-1" style="font-size: 12px;">
|
||||
{{ truncate(group.short_description, descriptionLength) }}
|
||||
</p>
|
||||
<p v-if="showStats" class="mb-0 small text-lighter">
|
||||
<span>
|
||||
<i class="fal fa-users"></i>
|
||||
<span class="small font-weight-bold">{{ prettyCount(group.member_count) }}</span>
|
||||
</span>
|
||||
|
||||
<span v-if="!group.local" class="remote-label ml-3">
|
||||
<i class="fal fa-globe"></i> Remote
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="group-card-inner-foaf">
|
||||
</div>
|
||||
|
||||
<div class="group-card-inner-cta">
|
||||
<router-link :to="`/groups/${group.id}`" class="btn btn-light btn-block font-weight-bold">
|
||||
Join Group
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
group: {
|
||||
type: Object
|
||||
},
|
||||
|
||||
compact: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
|
||||
showStats: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
|
||||
truncateTitleLength: {
|
||||
type: Number,
|
||||
default: 19
|
||||
},
|
||||
|
||||
truncateDescriptionLength: {
|
||||
type: Number,
|
||||
default: 22
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
titleLength: 40,
|
||||
descriptionLength: 60
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
if(this.compact) {
|
||||
this.titleLength = 19;
|
||||
this.descriptionLength = 22;
|
||||
}
|
||||
|
||||
if(this.truncateTitleLength != 19) {
|
||||
this.titleLength = this.truncateTitleLength;
|
||||
}
|
||||
|
||||
if(this.truncateDescriptionLength != 22) {
|
||||
this.descriptionLength = this.truncateDescriptionLength;
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
prettyCount(val) {
|
||||
return App.util.format.count(val);
|
||||
},
|
||||
|
||||
truncate(str, limit = 140) {
|
||||
if(str.length <= limit) {
|
||||
return str;
|
||||
}
|
||||
|
||||
return str.substr(0, limit) + ' ...';
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.group-card {
|
||||
margin-bottom: 15px;
|
||||
|
||||
&-inner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
|
||||
&-copy {
|
||||
background-color: var(--light);
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
&-foaf {
|
||||
background-color: var(--light);
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
&-cta {
|
||||
background-color: var(--light);
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.member-label {
|
||||
padding: 2px 5px;
|
||||
font-size: 9px;
|
||||
color: rgba(75, 119, 190, 1);
|
||||
background: rgba(137, 196, 244, 0.2);
|
||||
border: 1px solid rgba(137, 196, 244, 0.3);
|
||||
font-weight: 500;
|
||||
text-transform: capitalize;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.remote-label {
|
||||
padding: 2px 5px;
|
||||
font-size: 9px;
|
||||
color: #B45309;
|
||||
background: #FEF3C7;
|
||||
border: 1px solid #FCD34D;
|
||||
font-weight: 500;
|
||||
text-transform: capitalize;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.group-header-img {
|
||||
width: 100%;
|
||||
height: 150px;
|
||||
object-fit: cover;
|
||||
padding: 0px;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
</style>
|
345
resources/assets/components/groups/partials/GroupCompose.vue
Normal file
345
resources/assets/components/groups/partials/GroupCompose.vue
Normal file
|
@ -0,0 +1,345 @@
|
|||
<template>
|
||||
<div class="group-compose-form">
|
||||
<input ref="photoInput" id="photoInput" type="file" class="d-none file-input" accept="image/jpeg,image/png" @change="handlePhotoChange">
|
||||
<input ref="videoInput" id="videoInput" type="file" class="d-none file-input" accept="video/mp4" @change="handleVideoChange">
|
||||
<div class="card card-body border mb-3 shadow-sm rounded-lg">
|
||||
<div class="media align-items-top">
|
||||
<img v-if="profile" :src="profile.avatar" class="rounded-circle border mr-3" width="42px" height="42px" onerror="this.onerror=null;this.src='/storage/avatars/default.png?v=2'">
|
||||
<div class="media-body">
|
||||
<div class="d-block" style="min-height: 80px;">
|
||||
<div v-if="isUploading" class="w-100">
|
||||
<p class="font-weight-light mb-1">Uploading media ...</p>
|
||||
<div class="progress rounded-pill" style="height:4px">
|
||||
<div class="progress-bar" role="progressbar" :aria-valuenow="uploadProgress" aria-valuemin="0" aria-valuemax="100" :style="{ width: uploadProgress + '%'}"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="form-group mb-3">
|
||||
<textarea
|
||||
class="form-control"
|
||||
:class="{
|
||||
'form-control-lg': !composeText || composeText.length < 40,
|
||||
'rounded-pill': !composeText || composeText.length < 40,
|
||||
'bg-light': !composeText || composeText.length < 40,
|
||||
'border-0': !composeText || composeText.length < 40
|
||||
}"
|
||||
:rows="!composeText || composeText.length < 40 ? 1 : 5"
|
||||
:placeholder="placeholder"
|
||||
style="resize: none;"
|
||||
v-model="composeText"
|
||||
></textarea>
|
||||
|
||||
<div v-if="composeText" class="small text-muted mt-1" style="min-height: 20px;">
|
||||
<span class="float-right font-weight-bold">
|
||||
{{ composeText ? composeText.length : 0 }}/500
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="tab" class="tab">
|
||||
<div v-if="tab === 'poll'">
|
||||
<p class="font-weight-bold text-muted small">
|
||||
Poll Options
|
||||
</p>
|
||||
|
||||
<div v-if="pollOptions.length < 4" class="form-group mb-4">
|
||||
<input type="text" class="form-control rounded-pill" placeholder="Add a poll option, press enter to save" v-model="pollOptionModel" @keyup.enter="savePollOption">
|
||||
</div>
|
||||
|
||||
<div v-for="(option, index) in pollOptions" class="form-group mb-4 d-flex align-items-center" style="max-width:400px;position: relative;">
|
||||
<span class="font-weight-bold mr-2" style="position: absolute;left: 10px;">{{ index + 1 }}.</span>
|
||||
<input v-if="pollOptions[index].length < 50" type="text" class="form-control rounded-pill" placeholder="Add a poll option, press enter to save" v-model="pollOptions[index]" style="padding-left: 30px;padding-right: 90px;">
|
||||
<textarea v-else class="form-control" v-model="pollOptions[index]" placeholder="Add a poll option, press enter to save" rows="3" style="padding-left: 30px;padding-right:90px;"></textarea>
|
||||
<button class="btn btn-danger btn-sm rounded-pill font-weight-bold" style="position: absolute;right: 5px;" @click="deletePollOption(index)">
|
||||
<i class="fas fa-trash"></i> Delete
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="d-flex justify-content-between">
|
||||
<div>
|
||||
<p class="font-weight-bold text-muted small">
|
||||
Poll Expiry
|
||||
</p>
|
||||
|
||||
<div class="form-group">
|
||||
<select class="form-control rounded-pill" style="width: 200px;" v-model="pollExpiry">
|
||||
<option value="60">1 hour</option>
|
||||
<option value="360">6 hours</option>
|
||||
<option value="1440" selected>24 hours</option>
|
||||
<option value="10080">7 days</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!isUploading" class="">
|
||||
<div>
|
||||
<div v-if="photoName && photoName.length" class="bg-light rounded-pill mb-4 py-2">
|
||||
<div class="media align-items-center">
|
||||
<span style="width: 40px;height: 40px;border-radius:50px;opacity: 0.6;" class="d-flex align-items-center justify-content-center bg-primary mx-3">
|
||||
<i class="fal fa-image fa-lg text-white"></i>
|
||||
</span>
|
||||
<div class="media-body">
|
||||
<p class="mb-0 font-weight-bold text-muted">
|
||||
{{ photoName }}
|
||||
</p>
|
||||
</div>
|
||||
<button class="btn btn-link font-weight-bold text-decoration-none" @click.prevent="clearFileInputs">
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="videoName && videoName.length" class="bg-light rounded-pill mb-4 py-2">
|
||||
<div class="media align-items-center">
|
||||
<span style="width: 40px;height: 40px;border-radius:50px;opacity: 0.6;" class="d-flex align-items-center justify-content-center bg-primary mx-3">
|
||||
<i class="fal fa-video fa-lg text-white"></i>
|
||||
</span>
|
||||
<div class="media-body">
|
||||
<p class="mb-0 font-weight-bold text-muted">
|
||||
{{ videoName }}
|
||||
</p>
|
||||
</div>
|
||||
<button class="btn btn-link font-weight-bold text-decoration-none" @click.prevent="clearFileInputs">
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button
|
||||
class="btn btn-light border font-weight-bold py-1 px-2 rounded-lg mr-3"
|
||||
@click="switchTab('photo')"
|
||||
:disabled="photoName || videoName">
|
||||
<i class="fal fa-image mr-2"></i>
|
||||
<span>Add Photo</span>
|
||||
</button>
|
||||
<!-- <button
|
||||
class="btn btn-light border font-weight-bold py-1 px-2 rounded-lg mr-3"
|
||||
@click="switchTab('video')"
|
||||
:disabled="photoName || videoName">
|
||||
<i class="fal fa-video mr-2"></i>
|
||||
<span>Add Video</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="allowPolls"
|
||||
:class="[ tab == 'poll' ? 'btn-primary' : 'btn-light' ]"
|
||||
class="btn border font-weight-bold py-1 px-2 rounded-lg mr-3"
|
||||
@click="switchTab('poll')"
|
||||
:disabled="photoName || videoName">
|
||||
|
||||
<i class="fal fa-poll-h mr-2"></i>
|
||||
<span>Add Poll</span>
|
||||
</button> -->
|
||||
<!-- <button v-if="allowEvent" class="btn btn-light border font-weight-bold py-1 px-2 rounded-lg">
|
||||
<i class="fal fa-calendar-alt mr-1"></i>
|
||||
<span>Create Event</span>
|
||||
</button> -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p v-if="!isUploading && composeText && composeText.length > 1 || !isUploading && ['photo', 'video'].includes(tab)" class="mb-0">
|
||||
<button class="btn btn-primary font-weight-bold float-right px-5 rounded-pill mt-3" @click="newPost()" :disabled="isPosting">
|
||||
<span v-if="isPosting">
|
||||
<div class="spinner-border text-white spinner-border-sm" role="status">
|
||||
<span class="sr-only">Loading...</span>
|
||||
</div>
|
||||
</span>
|
||||
<span v-else>Post</span>
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script type="text/javascript">
|
||||
export default {
|
||||
props: {
|
||||
profile: {
|
||||
type: Object
|
||||
},
|
||||
|
||||
groupId: {
|
||||
type: String
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
config: window.App.config,
|
||||
composeText: undefined,
|
||||
tab: null,
|
||||
placeholder: 'Write something...',
|
||||
allowPhoto: true,
|
||||
allowVideo: true,
|
||||
allowPolls: true,
|
||||
allowEvent: true,
|
||||
pollOptionModel: null,
|
||||
pollOptions: [],
|
||||
pollExpiry: 1440,
|
||||
uploadProgress: 0,
|
||||
isUploading: false,
|
||||
isPosting: false,
|
||||
photoName: undefined,
|
||||
videoName: undefined
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
newPost() {
|
||||
if(this.isPosting) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isPosting = true;
|
||||
let self = this;
|
||||
let type = 'text';
|
||||
let data = new FormData();
|
||||
data.append('group_id', this.groupId);
|
||||
if(this.composeText && this.composeText.length) {
|
||||
data.append('caption', this.composeText);
|
||||
}
|
||||
|
||||
switch(this.tab) {
|
||||
case 'poll':
|
||||
if(!this.pollOptions || this.pollOptions.length < 2 || this.pollOptions.length > 4) {
|
||||
swal('Oops!', 'A poll must have 2-4 choices.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if(!this.composeText || this.composeText.length < 5) {
|
||||
swal('Oops!', 'A poll question must be at least 5 characters.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
for (var i = 0; i < this.pollOptions.length; i++) {
|
||||
data.append('pollOptions[]', this.pollOptions[i]);
|
||||
}
|
||||
|
||||
data.append('expiry', this.pollExpiry);
|
||||
type = 'poll';
|
||||
break;
|
||||
|
||||
case 'photo':
|
||||
data.append('photo', this.$refs.photoInput.files[0]);
|
||||
type = 'photo';
|
||||
this.isUploading = true;
|
||||
break;
|
||||
|
||||
case 'video':
|
||||
data.append('video', this.$refs.videoInput.files[0]);
|
||||
type = 'video';
|
||||
this.isUploading = true;
|
||||
break;
|
||||
}
|
||||
|
||||
data.append('type', type);
|
||||
|
||||
axios.post('/api/v0/groups/status/new', data, {
|
||||
onUploadProgress: function(progressEvent) {
|
||||
self.uploadProgress = Math.floor(progressEvent.loaded / progressEvent.total * 100);
|
||||
}
|
||||
})
|
||||
.then(res => {
|
||||
this.isPosting = false;
|
||||
this.isUploading = false;
|
||||
this.uploadProgress = 0;
|
||||
this.composeText = null;
|
||||
this.photo = null;
|
||||
this.tab = null;
|
||||
this.clearFileInputs(false);
|
||||
this.$emit('new-status', res.data);
|
||||
}).catch(err => {
|
||||
if(err.response.status == 422) {
|
||||
this.isPosting = false;
|
||||
this.isUploading = false;
|
||||
this.uploadProgress = 0;
|
||||
this.photo = null;
|
||||
this.tab = null;
|
||||
this.clearFileInputs(false);
|
||||
swal('Oops!', err.response.data.error, 'error');
|
||||
} else {
|
||||
this.isPosting = false;
|
||||
this.isUploading = false;
|
||||
this.uploadProgress = 0;
|
||||
this.photo = null;
|
||||
this.tab = null;
|
||||
this.clearFileInputs(false);
|
||||
swal('Oops!', 'An error occured while processing your request, please try again later', 'error');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
switchTab(newTab) {
|
||||
if(newTab == this.tab) {
|
||||
this.tab = null;
|
||||
this.placeholder = 'Write something...';
|
||||
return;
|
||||
}
|
||||
|
||||
switch(newTab) {
|
||||
case 'poll':
|
||||
this.placeholder = 'Write poll question here...'
|
||||
break;
|
||||
|
||||
case 'photo':
|
||||
this.$refs.photoInput.click();
|
||||
break;
|
||||
|
||||
case 'video':
|
||||
this.$refs.videoInput.click();
|
||||
break;
|
||||
|
||||
default:
|
||||
this.placeholder = 'Write something...';
|
||||
}
|
||||
|
||||
this.tab = newTab;
|
||||
},
|
||||
|
||||
savePollOption() {
|
||||
if(this.pollOptions.indexOf(this.pollOptionModel) != -1) {
|
||||
this.pollOptionModel = null;
|
||||
return;
|
||||
}
|
||||
this.pollOptions.push(this.pollOptionModel);
|
||||
this.pollOptionModel = null;
|
||||
},
|
||||
|
||||
deletePollOption(index) {
|
||||
this.pollOptions.splice(index, 1);
|
||||
},
|
||||
|
||||
handlePhotoChange() {
|
||||
event.currentTarget.blur();
|
||||
this.tab = 'photo';
|
||||
this.photoName = event.target.files[0].name;
|
||||
},
|
||||
|
||||
handleVideoChange() {
|
||||
event.currentTarget.blur();
|
||||
this.tab = 'video';
|
||||
this.videoName = event.target.files[0].name;
|
||||
},
|
||||
|
||||
clearFileInputs(blur = true) {
|
||||
if(blur) {
|
||||
event.currentTarget.blur();
|
||||
}
|
||||
this.tab = null;
|
||||
this.$refs.photoInput.value = null;
|
||||
this.photoName = null;
|
||||
this.$refs.videoInput.value = null;
|
||||
this.videoName = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.group-compose-form {
|
||||
}
|
||||
</style>
|
135
resources/assets/components/groups/partials/GroupInfoCard.vue
Normal file
135
resources/assets/components/groups/partials/GroupInfoCard.vue
Normal file
|
@ -0,0 +1,135 @@
|
|||
<template>
|
||||
<div class="group-info-card">
|
||||
<div class="card card-body mt-3 shadow-none border rounded-lg">
|
||||
<p class="title">About</p>
|
||||
|
||||
<p v-if="group.description && group.description.length > 1" class="description" v-html="group.description"></p>
|
||||
<p v-else class="description">This group does not have a description.</p>
|
||||
|
||||
</div>
|
||||
<div class="card card-body mt-3 shadow-none border rounded-lg">
|
||||
<div v-if="group.membership == 'all'" class="fact">
|
||||
<div class="fact-icon">
|
||||
<i class="fal fa-globe fa-lg"></i>
|
||||
</div>
|
||||
<div class="fact-body">
|
||||
<p class="fact-title">Public</p>
|
||||
<p class="fact-subtitle">Anyone can see who's in the group and what they post.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="group.membership == 'private'" class="fact">
|
||||
<div class="fact-icon">
|
||||
<i class="fal fa-lock fa-lg"></i>
|
||||
</div>
|
||||
<div class="fact-body">
|
||||
<p class="fact-title">Private</p>
|
||||
<p class="fact-subtitle">Only members can see who's in the group and what they post.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="group.config.discoverable == true" class="fact">
|
||||
<div class="fact-icon">
|
||||
<i class="fal fa-eye fa-lg"></i>
|
||||
</div>
|
||||
<div class="fact-body">
|
||||
<p class="fact-title">Visible</p>
|
||||
<p class="fact-subtitle">Anyone can find this group.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="group.config.discoverable == false" class="fact">
|
||||
<div class="fact-icon">
|
||||
<i class="fal fa-eye-slash fa-lg"></i>
|
||||
</div>
|
||||
<div class="fact-body">
|
||||
<p class="fact-title">Hidden</p>
|
||||
<p class="fact-subtitle">Only members can find this group.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- <div class="fact">
|
||||
<div class="fact-icon">
|
||||
<i class="fal fa-map-marker-alt fa-lg"></i>
|
||||
</div>
|
||||
<div class="fact-body">
|
||||
<p class="fact-title">Fediverse</p>
|
||||
<p class="fact-subtitle">This group has not specified a location.</p>
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<div class="fact">
|
||||
<div class="fact-icon">
|
||||
<i class="fal fa-users fa-lg"></i>
|
||||
</div>
|
||||
<div class="fact-body">
|
||||
<p class="fact-title"">{{ group.category.name }}</p>
|
||||
<p class="fact-subtitle">Category</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="mb-0 font-weight-light text-lighter">Created: {{ timestampFormat(group.created_at) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script type="text/javascript">
|
||||
export default {
|
||||
props: {
|
||||
group: {
|
||||
type: Object
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
timestampFormat(date, showTime = false) {
|
||||
let ts = new Date(date);
|
||||
return showTime ? ts.toDateString() + ' · ' + ts.toLocaleTimeString() : ts.toDateString();
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.group-info-card {
|
||||
.title {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: 15px;
|
||||
font-weight:400;
|
||||
color: #6c757d;
|
||||
margin-bottom: 0;
|
||||
white-space: break-spaces;
|
||||
}
|
||||
|
||||
.fact {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 1.5rem;
|
||||
|
||||
&-body {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
&-icon {
|
||||
width: 50px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&-title {
|
||||
font-size: 17px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
&-subtitle {
|
||||
font-size: 14px;
|
||||
margin-bottom: 0;
|
||||
color: #6c757d;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,60 @@
|
|||
<template>
|
||||
<div class="group-insights-component">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-12 col-md-3 mb-3">
|
||||
<div class="bg-light p-3 border rounded d-flex justify-content-between align-items-center">
|
||||
<p class="h3 font-weight-bold mb-0">124K</p>
|
||||
<p class="font-weight-bold text-uppercase text-lighter mb-0">Posts</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-3 mb-3">
|
||||
<div class="bg-light p-3 border rounded d-flex justify-content-between align-items-center">
|
||||
<p class="h3 font-weight-bold mb-0">9K</p>
|
||||
<p class="font-weight-bold text-uppercase text-lighter mb-0">Users</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-3 mb-3">
|
||||
<div class="bg-light p-3 border rounded d-flex justify-content-between align-items-center">
|
||||
<p class="h3 font-weight-bold mb-0">1.7M</p>
|
||||
<p class="font-weight-bold text-uppercase text-lighter mb-0">Interactions</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-12 col-md-3 mb-3">
|
||||
<div class="bg-light p-3 border rounded d-flex justify-content-between align-items-center">
|
||||
<p class="h3 font-weight-bold mb-0">50</p>
|
||||
<p class="font-weight-bold text-uppercase text-lighter mb-0">Mod Reports</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script type="text/javascript">
|
||||
export default {
|
||||
props: {
|
||||
group: {
|
||||
type: Object
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
|
||||
};
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.group-insights-component {
|
||||
|
||||
}
|
||||
</style>
|
190
resources/assets/components/groups/partials/GroupInviteModal.vue
Normal file
190
resources/assets/components/groups/partials/GroupInviteModal.vue
Normal file
|
@ -0,0 +1,190 @@
|
|||
<template>
|
||||
<div class="group-invite-modal">
|
||||
<b-modal
|
||||
ref="modal"
|
||||
hide-header
|
||||
hide-footer
|
||||
centered
|
||||
rounded
|
||||
body-class="rounded group-invite-modal-wrapper">
|
||||
|
||||
<div class="text-center py-3 d-flex align-items-center flex-column">
|
||||
<div class="bg-light rounded-circle d-flex justify-content-center align-items-center mb-3" style="width: 100px;height: 100px;">
|
||||
<i class="far fa-user-plus fa-2x text-lighter"></i>
|
||||
</div>
|
||||
<p class="h4 font-weight-bold mb-0">Invite Friends</p>
|
||||
<!-- <p class="mb-0">Search {{ group.name }} for posts, comments or members.</p> -->
|
||||
</div>
|
||||
|
||||
<transition name="fade">
|
||||
<div v-if="usernames.length < 5" class="d-flex justify-content-between mt-1">
|
||||
<autocomplete
|
||||
:search="autocompleteSearch"
|
||||
placeholder="Search friends by username"
|
||||
aria-label="Search this group"
|
||||
:get-result-value="getSearchResultValue"
|
||||
:debounceTime="700"
|
||||
@submit="onSearchSubmit"
|
||||
style="width: 100%;"
|
||||
ref="autocomplete"
|
||||
>
|
||||
<template #result="{ result, props }">
|
||||
<li
|
||||
v-bind="props"
|
||||
class="autocomplete-result"
|
||||
>
|
||||
<div class="text-truncate">
|
||||
<p class="result-name mb-0 font-weight-bold">
|
||||
{{ result.username }}
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
</template>
|
||||
</autocomplete>
|
||||
|
||||
<button class="btn btn-light border rounded-circle text-lighter ml-3" style="width: 52px;height:50px;" @click="close">
|
||||
<i class="fal fa-times fa-lg"></i>
|
||||
</button>
|
||||
</div>
|
||||
</transition>
|
||||
|
||||
<transition name="fade">
|
||||
<div v-if="usernames.length" class="pt-3">
|
||||
<div v-for="(result, index) in usernames" class="py-1">
|
||||
<div class="media align-items-center">
|
||||
<img src="/storage/avatars/default.jpg" class="rounded-circle border mr-3" width="45" height="45">
|
||||
<div class="media-body">
|
||||
<p class="lead mb-0">{{ result.username }}</p>
|
||||
</div>
|
||||
<button class="btn btn-link text-lighter btn-sm" @click="removeUsername(index)"><i class="far fa-times-circle fa-lg"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
|
||||
<transition name="fade">
|
||||
<button v-if="usernames && usernames.length" class="btn btn-primary btn-lg btn-block font-weight-bold rounded font-weight-bold mt-3" @click="submitInvites">Invite</button>
|
||||
</transition>
|
||||
|
||||
<div class="text-center pt-3 small">
|
||||
<p class="mb-0">You can invite up to 5 friends at a time, and 20 friends in total.</p>
|
||||
</div>
|
||||
</b-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script type="text/javascript">
|
||||
import Autocomplete from '@trevoreyre/autocomplete-vue'
|
||||
import '@trevoreyre/autocomplete-vue/dist/style.css'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
group: {
|
||||
type: Object
|
||||
},
|
||||
|
||||
profile: {
|
||||
type: Object
|
||||
}
|
||||
},
|
||||
|
||||
components: {
|
||||
'autocomplete-input': Autocomplete,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
query: '',
|
||||
recent: [],
|
||||
loaded: false,
|
||||
usernames: [],
|
||||
isSubmitting: false
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
open() {
|
||||
this.$refs.modal.show();
|
||||
},
|
||||
|
||||
close() {
|
||||
this.$refs.modal.hide();
|
||||
},
|
||||
|
||||
autocompleteSearch(q) {
|
||||
if (!q || q.length == 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return axios.post(`/api/v0/groups/search/invite/friends`, {
|
||||
q: q,
|
||||
g: this.group.id,
|
||||
v: '0.2'
|
||||
}).then(res => {
|
||||
let data = res.data.filter(r => {
|
||||
return this.usernames.map(u => u.username).indexOf(r.username) == -1;
|
||||
});
|
||||
return data;
|
||||
});
|
||||
return [];
|
||||
},
|
||||
|
||||
getSearchResultValue(result) {
|
||||
return result.username;
|
||||
},
|
||||
|
||||
onSearchSubmit(result) {
|
||||
this.usernames.push(result);
|
||||
this.$refs.autocomplete.value = '';
|
||||
//
|
||||
},
|
||||
|
||||
removeUsername(index) {
|
||||
event.currentTarget.blur();
|
||||
this.usernames.splice(index, 1);
|
||||
},
|
||||
|
||||
submitInvites() {
|
||||
this.isSubmitting = true;
|
||||
event.currentTarget.blur();
|
||||
axios.post('/api/v0/groups/search/invite/friends/send', {
|
||||
g: this.group.id,
|
||||
uids: this.usernames.map(u => u.id)
|
||||
}).then(res => {
|
||||
this.usernames = [];
|
||||
this.isSubmitting = false;
|
||||
this.close();
|
||||
swal('Success', 'Successfully sent invite(s)', 'success');
|
||||
}).catch(err => {
|
||||
this.usernames = [];
|
||||
this.isSubmitting = false;
|
||||
if(err.response.status === 422) {
|
||||
swal('Error', err.response.data.error, 'error');
|
||||
} else {
|
||||
swal('Oops!', 'An error occured, please try again later', 'error');
|
||||
}
|
||||
this.close();
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.group-invite-modal {
|
||||
|
||||
&-wrapper {
|
||||
.media {
|
||||
height: 60px;
|
||||
padding: 10px;
|
||||
border-radius: 10px;
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: #E5E7EB;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
156
resources/assets/components/groups/partials/GroupListCard.vue
Normal file
156
resources/assets/components/groups/partials/GroupListCard.vue
Normal file
|
@ -0,0 +1,156 @@
|
|||
<template>
|
||||
<div class="group-list-card">
|
||||
<div class="media">
|
||||
<div class="media align-items-center">
|
||||
<img
|
||||
v-if="group.metadata && group.metadata.hasOwnProperty('header')"
|
||||
:src="group.metadata.header.url"
|
||||
class="mr-3 border rounded group-header-img"
|
||||
:class="{ compact: compact }">
|
||||
|
||||
<div
|
||||
v-else
|
||||
class="mr-3 border rounded group-header-img"
|
||||
:class="{ compact: compact }">
|
||||
<div
|
||||
class="bg-primary d-flex align-items-center justify-content-center rounded"
|
||||
style="width: 100%; height:100%;">
|
||||
<i class="fal fa-users text-white fa-lg"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="media-body">
|
||||
<p class="font-weight-bold mb-0 text-dark" style="font-size: 16px;">
|
||||
{{ truncate(group.name || 'Untitled Group', titleLength) }}
|
||||
</p>
|
||||
|
||||
<p class="text-muted mb-1" style="font-size: 12px;">
|
||||
{{ truncate(group.short_description, descriptionLength) }}
|
||||
</p>
|
||||
|
||||
<p v-if="showStats" class="mb-0 small text-lighter">
|
||||
<span>
|
||||
<i class="far fa-users"></i>
|
||||
<span class="small font-weight-bold">{{ prettyCount(group.member_count) }}</span>
|
||||
</span>
|
||||
|
||||
<span v-if="!group.local" class="remote-label ml-3">
|
||||
<i class="fal fa-globe"></i> Remote
|
||||
</span>
|
||||
|
||||
<span v-if="group.hasOwnProperty('admin') && group.admin.hasOwnProperty('username')" class="ml-3">
|
||||
<i class="fal fa-user-crown"></i>
|
||||
<span class="small font-weight-bold">
|
||||
@{{ group.admin.username }}
|
||||
</span>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script type="text/javascript">
|
||||
export default {
|
||||
props: {
|
||||
group: {
|
||||
type: Object
|
||||
},
|
||||
|
||||
compact: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
|
||||
showStats: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
|
||||
truncateTitleLength: {
|
||||
type: Number,
|
||||
default: 19
|
||||
},
|
||||
|
||||
truncateDescriptionLength: {
|
||||
type: Number,
|
||||
default: 22
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
titleLength: 40,
|
||||
descriptionLength: 60
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
if(this.compact) {
|
||||
this.titleLength = 19;
|
||||
this.descriptionLength = 22;
|
||||
}
|
||||
|
||||
if(this.truncateTitleLength != 19) {
|
||||
this.titleLength = this.truncateTitleLength;
|
||||
}
|
||||
|
||||
if(this.truncateDescriptionLength != 22) {
|
||||
this.descriptionLength = this.truncateDescriptionLength;
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
prettyCount(val) {
|
||||
return App.util.format.count(val);
|
||||
},
|
||||
|
||||
truncate(str, limit = 140) {
|
||||
if(str.length <= limit) {
|
||||
return str;
|
||||
}
|
||||
|
||||
return str.substr(0, limit) + ' ...';
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.group-list-card {
|
||||
.member-label {
|
||||
padding: 2px 5px;
|
||||
font-size: 9px;
|
||||
color: rgba(75, 119, 190, 1);
|
||||
background: rgba(137, 196, 244, 0.2);
|
||||
border: 1px solid rgba(137, 196, 244, 0.3);
|
||||
font-weight: 500;
|
||||
text-transform: capitalize;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.remote-label {
|
||||
padding: 2px 5px;
|
||||
font-size: 9px;
|
||||
color: #B45309;
|
||||
background: #FEF3C7;
|
||||
border: 1px solid #FCD34D;
|
||||
font-weight: 500;
|
||||
text-transform: capitalize;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.group-header-img {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
object-fit: cover;
|
||||
padding:0px;
|
||||
|
||||
&.compact {
|
||||
width: 42.5px;
|
||||
height: 42.5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
262
resources/assets/components/groups/partials/GroupMedia.vue
Normal file
262
resources/assets/components/groups/partials/GroupMedia.vue
Normal file
|
@ -0,0 +1,262 @@
|
|||
<template>
|
||||
<div class="group-media-component">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-12">
|
||||
<div class="card card-body border shadow-sm">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<p class="h4 font-weight-bold mb-0">Media</p>
|
||||
<!-- <div>
|
||||
<a href="#" class="font-weight-bold mr-3"><i class="fas fa-plus fa-sm"></i> Create Album</a>
|
||||
<a href="#" class="font-weight-bold">Add Photos/Video</a>
|
||||
</div> -->
|
||||
</div>
|
||||
|
||||
<div v-if="isLoaded">
|
||||
<div class="mb-5">
|
||||
<button
|
||||
:class="[ tab == 'photo' ? 'text-primary font-weight-bold' : 'text-lighter' ]"
|
||||
class="btn btn-light mr-2"
|
||||
@click="switchTab('photo')">
|
||||
Photos
|
||||
</button>
|
||||
<button
|
||||
:class="[ tab == 'video' ? 'text-primary font-weight-bold' : 'text-lighter' ]"
|
||||
class="btn btn-light mr-2"
|
||||
@click="switchTab('video')">
|
||||
Videos
|
||||
</button>
|
||||
<button
|
||||
:class="[ tab == 'album' ? 'text-primary font-weight-bold' : 'text-lighter' ]"
|
||||
class="btn btn-light mr-2"
|
||||
@click="switchTab('album')">
|
||||
Albums
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="tab == 'photo'" class="row px-3">
|
||||
<div v-for="(status, index) in photos" class="m-1">
|
||||
<a :href="status.url" class="bh-content">
|
||||
<img :src="getMediaSource(status)" width="205" height="205" style="object-fit: cover;">
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div v-if="photos.length == 0" class="col-12 py-5 text-center">
|
||||
<p class="lead font-weight-bold mb-0">No photos found</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="tab == 'video'" class="row px-3">
|
||||
<div v-for="(status, index) in videos" class="m-1">
|
||||
<a :href="status.url" class="bh-content text-decoration-none">
|
||||
<img v-if="!status.media_attachments[0].preview_url.endsWith('no-preview.png')" :src="getMediaSource(status)" width="205" height="205" style="object-fit: cover;">
|
||||
<div v-else class="bg-light text-dark d-flex align-items-center justify-content-center border" style="width:205px;height:205px;">
|
||||
<p class="font-weight-bold mb-0">No preview available</p>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div v-if="videos.length == 0" class="col-12 py-5 text-center">
|
||||
<p class="lead font-weight-bold mb-0">No videos found</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="tab == 'album'" class="row px-3">
|
||||
<div v-for="(status, index) in albums" class="m-1">
|
||||
<a :href="status.url" class="bh-content">
|
||||
<img :src="getMediaSource(status)" width="205" height="205" style="object-fit: cover;">
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div v-if="albums.length == 0" class="col-12 py-5 text-center">
|
||||
<p class="lead font-weight-bold mb-0">No albums found</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="hasNextPage[tab]" class="mt-3">
|
||||
<button class="btn btn-light font-weight-bold btn-block border" @click="loadNextPage">Load more</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="d-flex align-items-center justify-content-center" style="height:500px">
|
||||
<div class="spinner-border text-primary" role="status">
|
||||
<span class="sr-only">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script type="text/javascript">
|
||||
export default {
|
||||
props: {
|
||||
group: {
|
||||
type: Object
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
isLoaded: false,
|
||||
feed: [],
|
||||
photos: [],
|
||||
videos: [],
|
||||
albums: [],
|
||||
tab: 'photo',
|
||||
tabs: [
|
||||
'photo',
|
||||
'video',
|
||||
'album'
|
||||
],
|
||||
page: {
|
||||
'photo': 1,
|
||||
'video': 1,
|
||||
'album': 1
|
||||
},
|
||||
hasNextPage: {
|
||||
'photo': false,
|
||||
'video': false,
|
||||
'album': false
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.fetchMedia();
|
||||
},
|
||||
|
||||
methods: {
|
||||
fetchMedia() {
|
||||
axios.get('/api/v0/groups/media/list', {
|
||||
params: {
|
||||
gid: this.group.id,
|
||||
page: this.page[this.tab],
|
||||
type: this.tab
|
||||
}
|
||||
}).then(res => {
|
||||
if(res.data.length > 0) {
|
||||
this.hasNextPage[this.tab] = true;
|
||||
}
|
||||
|
||||
this.isLoaded = true;
|
||||
|
||||
res.data.forEach(status => {
|
||||
if(status.pf_type == 'photo') {
|
||||
this.photos.push(status);
|
||||
}
|
||||
|
||||
if(status.pf_type == 'video') {
|
||||
this.videos.push(status);
|
||||
}
|
||||
|
||||
if(status.pf_type == 'photo:album') {
|
||||
this.albums.push(status);
|
||||
}
|
||||
})
|
||||
this.page[this.tab] = this.page[this.tab] + 1;
|
||||
}).catch(err => {
|
||||
this.hasNextPage[this.tab] = false;
|
||||
console.log(err.response);
|
||||
})
|
||||
},
|
||||
|
||||
loadNextPage() {
|
||||
axios.get('/api/v0/groups/media/list', {
|
||||
params: {
|
||||
gid: this.group.id,
|
||||
page: this.page[this.tab],
|
||||
type: this.tab,
|
||||
}
|
||||
}).then(res => {
|
||||
if(res.data.length == 0) {
|
||||
this.hasNextPage[this.tab] = false;
|
||||
return;
|
||||
}
|
||||
res.data.forEach(status => {
|
||||
if(status.pf_type == 'photo') {
|
||||
this.photos.push(status);
|
||||
}
|
||||
|
||||
if(status.pf_type == 'video') {
|
||||
this.videos.push(status);
|
||||
}
|
||||
|
||||
if(status.pf_type == 'photo:album') {
|
||||
this.albums.push(status);
|
||||
}
|
||||
})
|
||||
this.page[this.tab] = this.page[this.tab] + 1;
|
||||
}).catch(err => {
|
||||
this.hasNextPage[this.tab] = false;
|
||||
})
|
||||
},
|
||||
|
||||
formatDate(ts) {
|
||||
return new Date(ts).toDateString();
|
||||
},
|
||||
|
||||
switchTab(tab) {
|
||||
this.tab = tab;
|
||||
this.fetchMedia();
|
||||
},
|
||||
|
||||
lightbox(status) {
|
||||
this.lightboxStatus = status.media_attachments[0];
|
||||
this.$refs.lightboxModal.show();
|
||||
},
|
||||
|
||||
hideLightbox() {
|
||||
this.lightboxStatus = null;
|
||||
this.$refs.lightboxModal.hide();
|
||||
},
|
||||
|
||||
blurhashWidth(status) {
|
||||
if(!status.media_attachments[0].meta) {
|
||||
return 25;
|
||||
}
|
||||
let aspect = status.media_attachments[0].meta.original.aspect;
|
||||
if(aspect == 1) {
|
||||
return 25;
|
||||
} else if(aspect > 1) {
|
||||
return 30;
|
||||
} else {
|
||||
return 20;
|
||||
}
|
||||
},
|
||||
|
||||
blurhashHeight(status) {
|
||||
if(!status.media_attachments[0].meta) {
|
||||
return 25;
|
||||
}
|
||||
let aspect = status.media_attachments[0].meta.original.aspect;
|
||||
if(aspect == 1) {
|
||||
return 25;
|
||||
} else if(aspect > 1) {
|
||||
return 20;
|
||||
} else {
|
||||
return 30;
|
||||
}
|
||||
},
|
||||
|
||||
getMediaSource(status) {
|
||||
let media = status.media_attachments[0];
|
||||
|
||||
if(media.preview_url && media.preview_url.endsWith('storage/no-preview.png')) {
|
||||
return media.url;
|
||||
}
|
||||
|
||||
if(media.preview_url && media.preview_url.length) {
|
||||
return media.url;
|
||||
}
|
||||
|
||||
return media.url;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.group-members-component {
|
||||
|
||||
}
|
||||
</style>
|
684
resources/assets/components/groups/partials/GroupMembers.vue
Normal file
684
resources/assets/components/groups/partials/GroupMembers.vue
Normal file
|
@ -0,0 +1,684 @@
|
|||
<template>
|
||||
<div class="group-members-component">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-12 col-md-8 mb-5">
|
||||
<div v-if="isAdmin && requestCount && !hideHeader" class="card card-body border shadow-sm bg-dark text-light mb-4 rounded-pill p-2 pl-3">
|
||||
<div class="d-flex align-items-center justify-content-between">
|
||||
<span class="lead mb-0 text-lighter">
|
||||
<i class="fal fa-exclamation-triangle mr-2 text-warning"></i>
|
||||
You have <strong class="text-white">{{ requestCount }}</strong> member applications to review
|
||||
</span>
|
||||
<span>
|
||||
<button class="btn btn-primary font-weight-bold rounded-pill btn-sm px-3" @click="reviewApplicants()">Review</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card card-body border shadow-sm">
|
||||
<div v-if="!hideHeader">
|
||||
<p class="d-flex align-items-center mb-0">
|
||||
<span class="lead font-weight-bold">Members</span>
|
||||
<span class="mx-2">·</span>
|
||||
<span class="text-muted">{{group.member_count}}</span>
|
||||
</p>
|
||||
<!-- <p class="text-muted mb-0">
|
||||
New people who join this group will appear here. <a class="font-weight-bold text-dark" href="#">Learn More</a>
|
||||
</p> -->
|
||||
<div class="form-group mt-3" style="position: relative;">
|
||||
<i class="fas fa-search fa-lg text-lighter" style="position: absolute;left:20px; top:50%;transform:translateY(-50%);"></i>
|
||||
<input class="form-control form-control-lg bg-light border rounded-pill" style="padding-left: 50px;padding-right: 50px;" placeholder="Find a member" v-model="memberSearchModel">
|
||||
<button class="btn btn-primary font-weight-bold rounded-pill px-3" style="position: absolute;right: 6px; top: 50%;transform: translateY(-50%);">Search</button>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
</div>
|
||||
|
||||
<div v-if="tab == 'list'">
|
||||
<div class="group-members-component-paginated-list py-3">
|
||||
<div class="media align-items-center">
|
||||
<a :href="profile.url" class="text-dark text-decoration-none">
|
||||
<img
|
||||
:src="profile?.avatar"
|
||||
width="56"
|
||||
height="56"
|
||||
class="rounded-circle border mr-2"
|
||||
onerror="this.onerror=null;this.src='/storage/avatars/default.png?v=2'"
|
||||
/>
|
||||
</a>
|
||||
<div class="media-body">
|
||||
<p class="lead font-weight-bold mb-0">
|
||||
{{profile.username}}
|
||||
<span class="member-label rounded ml-1">Me</span>
|
||||
</p>
|
||||
<p class="text-muted mb-0">Founded group {{formatDate(group.created_at)}}</p>
|
||||
</div>
|
||||
<!-- <button class="btn btn-light border">
|
||||
<i class="fas fa-ellipsis-h text-muted"></i>
|
||||
</button> -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr v-if="mutual.length > 0">
|
||||
|
||||
<div v-if="mutual.length > 0" class="group-members-component-paginated-list">
|
||||
<p class="font-weight-bold mb-1">Mutual Friends</p>
|
||||
|
||||
<div v-for="(member, index) in mutual" class="media align-items-center py-3">
|
||||
<a :href="member.url" class="text-dark text-decoration-none">
|
||||
<img
|
||||
:src="member?.avatar"
|
||||
width="56"
|
||||
height="56"
|
||||
class="rounded-circle border mr-2"
|
||||
onerror="this.onerror=null;this.src='/storage/avatars/default.png?v=2'"
|
||||
/>
|
||||
</a>
|
||||
|
||||
<div class="media-body">
|
||||
<p class="lead font-weight-bold mb-0">
|
||||
<a :href="member.url" class="text-dark text-decoration-none" :title="member.acct" data-toggle="tooltip">{{member.username}}</a>
|
||||
<span v-if="member.role == 'founder'" class="member-label rounded ml-1">Admin</span>
|
||||
<span v-if="!member.local" class="remote-label rounded ml-1">Remote</span>
|
||||
</p>
|
||||
<p class="text-muted mb-0">Member since {{formatDate(member.joined)}}</p>
|
||||
</div>
|
||||
<a
|
||||
class="btn btn-light lead font-weight-bolder px-3 border"
|
||||
:href="'/account/direct/t/' + member.id">
|
||||
<i class="far fa-comment-dots mr-1"></i> Message
|
||||
</a>
|
||||
<b-dropdown
|
||||
v-if="isAdmin"
|
||||
toggle-class="btn btn-light font-weight-bold px-3 border ml-2"
|
||||
toggle-tag="a"
|
||||
:lazy="true"
|
||||
right
|
||||
no-caret>
|
||||
<template #button-content>
|
||||
<i class="fas fa-ellipsis-h"></i>
|
||||
</template>
|
||||
<b-dropdown-item :href="member.url">View Profile</b-dropdown-item>
|
||||
<b-dropdown-item :href="'/account/direct/t/'+member.id">Send Message</b-dropdown-item>
|
||||
<b-dropdown-item>View Activity</b-dropdown-item>
|
||||
<b-dropdown-divider></b-dropdown-divider>
|
||||
<b-dropdown-item link-class="font-weight-bold" @click.prevent="openInteractionLimitModal(member)">Limit interactions</b-dropdown-item>
|
||||
<b-dropdown-item link-class="font-weight-bold text-danger">Remove from group</b-dropdown-item>
|
||||
</b-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr v-if="members.length > 0">
|
||||
|
||||
<div v-if="members.length > 0" class="group-members-component-paginated-list">
|
||||
<p class="font-weight-bold mb-1">Other Members</p>
|
||||
|
||||
<div v-for="(member, index) in members" class="media align-items-center py-3">
|
||||
<a :href="member.url" class="text-dark text-decoration-none">
|
||||
<img
|
||||
:src="member?.avatar"
|
||||
width="56"
|
||||
height="56"
|
||||
class="rounded-circle border mr-2"
|
||||
onerror="this.onerror=null;this.src='/storage/avatars/default.png?v=2'"
|
||||
/>
|
||||
</a>
|
||||
|
||||
<div class="media-body">
|
||||
<p class="lead font-weight-bold mb-0">
|
||||
<a :href="member.url" class="text-dark text-decoration-none" :title="member.acct" data-toggle="tooltip">{{member.username}}</a>
|
||||
<span v-if="member.is_admin" class="member-label rounded ml-1">Admin</span>
|
||||
<span v-if="!member.local" class="remote-label rounded ml-1">Remote</span>
|
||||
</p>
|
||||
<p class="text-muted mb-0">Member since {{formatDate(member.joined)}}</p>
|
||||
</div>
|
||||
<button
|
||||
:class="[ member.following ? 'btn-primary' : 'btn-light']"
|
||||
class="btn lead font-weight-bolder px-4 border"
|
||||
@click="follow(index)">
|
||||
<span v-if="member.following">
|
||||
Following
|
||||
</span>
|
||||
<span v-else>
|
||||
<i class="fas fa-user-plus mr-2"></i> Follow
|
||||
</span>
|
||||
</button>
|
||||
<b-dropdown
|
||||
v-if="isAdmin"
|
||||
toggle-class="btn btn-light font-weight-bold px-3 border ml-2"
|
||||
toggle-tag="a"
|
||||
:lazy="true"
|
||||
right
|
||||
no-caret>
|
||||
<template #button-content>
|
||||
<i class="fas fa-ellipsis-h"></i>
|
||||
</template>
|
||||
<b-dropdown-item :href="member.url" link-class="font-weight-bold">View Profile</b-dropdown-item>
|
||||
<b-dropdown-item :href="'/account/direct/t/'+member.id" link-class="font-weight-bold">Send Message</b-dropdown-item>
|
||||
<b-dropdown-item link-class="font-weight-bold">View Activity</b-dropdown-item>
|
||||
<b-dropdown-divider></b-dropdown-divider>
|
||||
<b-dropdown-item link-class="font-weight-bold" @click.prevent="openInteractionLimitModal(member)">Limit interactions</b-dropdown-item>
|
||||
<b-dropdown-item link-class="font-weight-bold text-danger">Remove from group</b-dropdown-item>
|
||||
</b-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p v-if="members.length > 0 && hasNextPage" class="mt-4">
|
||||
<button class="btn btn-light btn-block border font-weight-bold" @click="loadNextPage">Load more</button>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-if="tab == 'search'" class="d-flex justify-content-center align-items-center" style="min-height: 100px;">
|
||||
<div class="text-center text-muted">
|
||||
<div class="spinner-border" role="status">
|
||||
<span class="sr-only">Loading...</span>
|
||||
</div>
|
||||
<p class="lead mb-0 mt-2">Loading results ...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="tab == 'results'">
|
||||
<div v-if="results.length > 0" class="group-members-component-paginated-list">
|
||||
<p class="font-weight-bold mb-1">Results</p>
|
||||
|
||||
<div v-for="(member, index) in results" class="media align-items-center py-3">
|
||||
<a :href="member.url" class="text-dark text-decoration-none">
|
||||
<img
|
||||
:src="member?.avatar"
|
||||
width="56"
|
||||
height="56"
|
||||
class="rounded-circle border mr-2"
|
||||
onerror="this.onerror=null;this.src='/storage/avatars/default.png?v=2'"
|
||||
/>
|
||||
</a>
|
||||
|
||||
<div class="media-body">
|
||||
<p class="lead font-weight-bold mb-0">
|
||||
<a :href="member.url" class="text-dark text-decoration-none" :title="member.acct" data-toggle="tooltip">{{member.username}}</a>
|
||||
<span v-if="member.role == 'founder'" class="member-label rounded ml-1">Admin</span>
|
||||
<span v-if="!member.local" class="remote-label rounded ml-1">Remote</span>
|
||||
</p>
|
||||
<p class="text-muted mb-0">Member since {{formatDate(member.joined)}}</p>
|
||||
</div>
|
||||
<a
|
||||
class="btn btn-light lead font-weight-bolder px-3 border"
|
||||
:href="'/account/direct/t/' + member.id">
|
||||
<i class="far fa-comment-dots mr-1"></i> Message
|
||||
</a>
|
||||
<b-dropdown
|
||||
v-if="isAdmin"
|
||||
toggle-class="btn btn-light font-weight-bold px-3 border ml-2"
|
||||
toggle-tag="a"
|
||||
:lazy="true"
|
||||
right
|
||||
no-caret>
|
||||
<template #button-content>
|
||||
<i class="fas fa-ellipsis-h"></i>
|
||||
</template>
|
||||
<b-dropdown-item :href="member.url">View Profile</b-dropdown-item>
|
||||
<b-dropdown-item :href="'/account/direct/t/'+member.id">Send Message</b-dropdown-item>
|
||||
<b-dropdown-item>View Activity</b-dropdown-item>
|
||||
<b-dropdown-divider></b-dropdown-divider>
|
||||
<b-dropdown-item link-class="font-weight-bold" @click.prevent="openInteractionLimitModal(member)">Limit interactions</b-dropdown-item>
|
||||
<b-dropdown-item link-class="font-weight-bold text-danger">Remove from group</b-dropdown-item>
|
||||
</b-dropdown>
|
||||
</div>
|
||||
<p class="text-center mt-5">
|
||||
<a href="#" class="font-weight-bold" @click="backFromSearch">Go back</a>
|
||||
</p>
|
||||
</div>
|
||||
<div v-else class="text-center text-muted mt-3">
|
||||
<p class="lead">No results found.</p>
|
||||
<p>
|
||||
<a href="#" class="font-weight-bold" @click="backFromSearch">Go back</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="tab == 'memberInteractionLimits'">
|
||||
<div v-if="results.length > 0" class="group-members-component-paginated-list">
|
||||
<p class="font-weight-bold mb-1">Interaction Limits</p>
|
||||
|
||||
<div v-for="(member, index) in results" class="media align-items-center py-3">
|
||||
<a :href="member.url" class="text-dark text-decoration-none">
|
||||
<img
|
||||
:src="member?.avatar"
|
||||
width="56"
|
||||
height="56"
|
||||
class="rounded-circle border mr-2"
|
||||
onerror="this.onerror=null;this.src='/storage/avatars/default.png?v=2'"
|
||||
/>
|
||||
</a>
|
||||
|
||||
<div class="media-body">
|
||||
<p class="lead font-weight-bold mb-0">
|
||||
<a :href="member.url" class="text-dark text-decoration-none">{{member.username}}</a>
|
||||
<span v-if="member.role == 'founder'" class="member-label rounded ml-1">Admin</span>
|
||||
</p>
|
||||
<p class="text-muted mb-0">Member since {{formatDate(member.joined)}}</p>
|
||||
</div>
|
||||
<a
|
||||
class="btn btn-light lead font-weight-bolder px-3 border"
|
||||
:href="'/account/direct/t/' + member.id">
|
||||
<i class="far fa-comment-dots mr-1"></i> Message
|
||||
</a>
|
||||
<b-dropdown
|
||||
v-if="isAdmin"
|
||||
toggle-class="btn btn-light font-weight-bold px-3 border ml-2"
|
||||
toggle-tag="a"
|
||||
:lazy="true"
|
||||
right
|
||||
no-caret>
|
||||
<template #button-content>
|
||||
<i class="fas fa-ellipsis-h"></i>
|
||||
</template>
|
||||
<b-dropdown-item :href="member.url">View Profile</b-dropdown-item>
|
||||
<b-dropdown-item :href="'/account/direct/t/'+member.id">Send Message</b-dropdown-item>
|
||||
<b-dropdown-item>View Activity</b-dropdown-item>
|
||||
<b-dropdown-divider></b-dropdown-divider>
|
||||
<b-dropdown-item link-class="font-weight-bold" @click.prevent="openInteractionLimitModal(member)">Limit interactions</b-dropdown-item>
|
||||
<b-dropdown-item link-class="font-weight-bold text-danger">Remove from group</b-dropdown-item>
|
||||
</b-dropdown>
|
||||
</div>
|
||||
<p class="text-center mt-5">
|
||||
<a href="#" class="font-weight-bold" @click.prevent="backFromSearch">Go back</a>
|
||||
</p>
|
||||
</div>
|
||||
<div v-else class="text-center text-muted mt-3">
|
||||
<p class="lead">No results found.</p>
|
||||
<p>
|
||||
<a href="#" class="font-weight-bold" @click.prevent="backFromSearch">Go back</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="tab == 'review'">
|
||||
<div v-if="reviewsLoaded">
|
||||
|
||||
<div class="group-members-component-paginated-list">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div class="d-flex">
|
||||
<button class="btn btn-link btn-sm mr-2" @click="backFromReview">
|
||||
<i class="far fa-chevron-left fa-lg"></i>
|
||||
</button>
|
||||
|
||||
<p class="font-weight-bold mb-0">Review Membership Applicants</p>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
|
||||
<div v-for="(member, index) in applicants" class="media align-items-center py-3">
|
||||
<a :href="member.url" class="text-dark text-decoration-none">
|
||||
<img
|
||||
:src="member?.avatar"
|
||||
width="56"
|
||||
height="56"
|
||||
class="rounded-circle border mr-2"
|
||||
onerror="this.onerror=null;this.src='/storage/avatars/default.png?v=2'"
|
||||
/>
|
||||
</a>
|
||||
|
||||
<div class="media-body">
|
||||
<p class="lead font-weight-bold mb-0">
|
||||
<a :href="member.url" class="text-dark text-decoration-none" :title="member.acct" data-toggle="tooltip">{{member.username}}</a>
|
||||
<span v-if="!member.local" class="remote-label rounded ml-1">Remote</span>
|
||||
</p>
|
||||
<p class="text-muted mb-0 small">
|
||||
<span>
|
||||
{{ member.followers_count }} Followers
|
||||
</span>
|
||||
|
||||
<span class="mx-1">·</span>
|
||||
|
||||
<span>
|
||||
Joined {{formatDate(member.created_at)}}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-light lead font-weight-bolder px-3 border"
|
||||
@click="handleApplicant(index, 'ignore')">
|
||||
<i class="far fa-times mr-1"></i> Ignore
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-danger lead font-weight-bolder px-3 border"
|
||||
@click="handleApplicant(index, 'reject')">
|
||||
<i class="far fa-times mr-1"></i> Reject
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary lead font-weight-bolder px-3 border"
|
||||
@click="handleApplicant(index, 'approve')">
|
||||
<i class="far fa-check mr-1"></i> Approve
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
v-if="applicantsCanLoadMore"
|
||||
class="btn btn-light font-weight-bold btn-block"
|
||||
@click="loadMoreApplicants"
|
||||
:disabled="loadingMoreApplicants">
|
||||
Load More
|
||||
</button>
|
||||
|
||||
<div v-if="!applicants || !applicants.length">
|
||||
<p class="text-center lead mb-0">No content found</p>
|
||||
<p class="text-center mb-0">
|
||||
<a class="font-weight-bold" href="#" @click.prevent="backFromReview">Go back</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div v-else class="d-flex align-items-center justify-content-center" style="min-height: 100px;">
|
||||
<b-spinner variant="muted" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="tab == 'loading'" class="d-flex align-items-center justify-content-center" style="min-height: 100px;">
|
||||
<b-spinner variant="muted" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<group-interaction-limits-modal
|
||||
ref="interactionModal"
|
||||
:group="group"
|
||||
:profile="activeProfile"
|
||||
/>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script type="text/javascript">
|
||||
import InteractionModal from './MemberLimitInteractionsModal.vue';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
group: {
|
||||
type: Object
|
||||
},
|
||||
|
||||
profile: {
|
||||
type: Object
|
||||
},
|
||||
|
||||
requestCount: {
|
||||
type: Number
|
||||
},
|
||||
|
||||
isAdmin: {
|
||||
type: Boolean
|
||||
}
|
||||
},
|
||||
|
||||
components: {
|
||||
'group-interaction-limits-modal': InteractionModal
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
members: [],
|
||||
mutual: [],
|
||||
results: [],
|
||||
page: 1,
|
||||
hasNextPage: true,
|
||||
tab: 'list',
|
||||
memberSearchModel: null,
|
||||
activeProfile: undefined,
|
||||
hideHeader: false,
|
||||
reviewsLoaded: false,
|
||||
applicants: [],
|
||||
applicantsPage: 1,
|
||||
applicantsCanLoadMore: false,
|
||||
loadingMoreApplicants: false
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.fetchMembers();
|
||||
let u = new URLSearchParams(window.location.search);
|
||||
|
||||
if(this.group.self.role == 'founder') {
|
||||
this.isAdmin = true;
|
||||
|
||||
if(u.has('a')) {
|
||||
if(u.get('a') == 'il') {
|
||||
this.tab = 'loading';
|
||||
let pid = u.get('pid');
|
||||
axios.get('/api/v0/groups/members/get', {
|
||||
params: {
|
||||
gid: this.group.id,
|
||||
pid: pid
|
||||
}
|
||||
})
|
||||
.then(res => {
|
||||
this.results.push(res.data);
|
||||
this.tab = 'memberInteractionLimits';
|
||||
this.openInteractionLimitModal(res.data);
|
||||
});
|
||||
}
|
||||
|
||||
if(u.get('a') == 'review') {
|
||||
this.reviewApplicants();
|
||||
}
|
||||
}
|
||||
} else if (u.has('a')) {
|
||||
history.pushState(null, null, `/groups/${this.group.id}/members`);
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
watch: {
|
||||
memberSearchModel: function(val) {
|
||||
this.handleSearch();
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
fetchMembers() {
|
||||
axios.get('/api/v0/groups/members/list', {
|
||||
params: {
|
||||
gid: this.group.id,
|
||||
page: this.page
|
||||
}
|
||||
}).then(res => {
|
||||
let data = res.data.filter(m => {
|
||||
return m.id != this.profile.id;
|
||||
});
|
||||
this.members = data.filter(m => {
|
||||
return !m.following
|
||||
});
|
||||
this.mutual = data.filter(m => {
|
||||
return m.following
|
||||
});
|
||||
this.page++;
|
||||
this.$nextTick(() => {
|
||||
$('[data-toggle="tooltip"]').tooltip()
|
||||
});
|
||||
}).catch(err => {
|
||||
console.log(res.response);
|
||||
})
|
||||
},
|
||||
|
||||
loadNextPage() {
|
||||
axios.get('/api/v0/groups/members/list', {
|
||||
params: {
|
||||
gid: this.group.id,
|
||||
page: this.page
|
||||
}
|
||||
}).then(res => {
|
||||
if(res.data.length == 0) {
|
||||
this.hasNextPage = false;
|
||||
return;
|
||||
}
|
||||
|
||||
let data = res.data.filter(m => {
|
||||
return m.id != this.profile.id;
|
||||
});
|
||||
this.members.push(...data.filter(m => {
|
||||
return !m.following
|
||||
}));
|
||||
this.mutual.push(...data.filter(m => {
|
||||
return m.following
|
||||
}));
|
||||
this.page++;
|
||||
this.$nextTick(() => {
|
||||
$('[data-toggle="tooltip"]').tooltip()
|
||||
});
|
||||
}).catch(err => {
|
||||
console.log(err.response);
|
||||
})
|
||||
},
|
||||
|
||||
follow(index) {
|
||||
axios.post('/i/follow', {
|
||||
item: this.members[index].id
|
||||
}).then(res => {
|
||||
this.members[index].following = !this.members[index].following;
|
||||
}).catch(err => {
|
||||
console.log(err.response);
|
||||
})
|
||||
},
|
||||
|
||||
formatDate(ts) {
|
||||
return new Date(ts).toDateString();
|
||||
},
|
||||
|
||||
handleSearch() {
|
||||
if(!this.memberSearchModel || this.memberSearchModel == "" || this.memberSearchModel.length == 0) {
|
||||
this.tab == 'list';
|
||||
this.memberSearchModel = null;
|
||||
return;
|
||||
}
|
||||
|
||||
this.tab = 'search';
|
||||
this.results = this.members.concat(this.mutual).filter(profile => {
|
||||
return profile.username.includes(this.memberSearchModel);
|
||||
});
|
||||
|
||||
// if(this.results.length) {
|
||||
this.tab = 'results';
|
||||
// }
|
||||
},
|
||||
|
||||
backFromSearch() {
|
||||
event.currentTarget.blur();
|
||||
this.memberSearchModel = null;
|
||||
this.tab = 'list';
|
||||
history.pushState(null, null, `/groups/${this.group.id}/members`);
|
||||
},
|
||||
|
||||
openInteractionLimitModal(member) {
|
||||
this.activeProfile = member;
|
||||
setTimeout(() => {
|
||||
this.$refs.interactionModal.open();
|
||||
}, 500);
|
||||
},
|
||||
|
||||
reviewApplicants() {
|
||||
this.hideHeader = true;
|
||||
this.tab = 'review';
|
||||
history.pushState(null, null, `/groups/${this.group.id}/members?a=review`);
|
||||
|
||||
axios.get('/api/v0/groups/members/requests', {
|
||||
params: {
|
||||
gid: this.group.id
|
||||
}
|
||||
})
|
||||
.then(res => {
|
||||
this.applicants = res.data;
|
||||
this.reviewsLoaded = true;
|
||||
this.applicantsPage = 2;
|
||||
this.applicantsCanLoadMore = res.data.length == 10;
|
||||
})
|
||||
},
|
||||
|
||||
handleApplicant(index, action) {
|
||||
event.currentTarget.blur();
|
||||
|
||||
if(action == 'ignore') {
|
||||
this.applicants.splice(index, 1);
|
||||
return;
|
||||
}
|
||||
|
||||
this.tab = 'loading';
|
||||
|
||||
if(!window.confirm('Are you sure you want to perform this action?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
axios.post('/api/v0/groups/members/request', {
|
||||
gid: this.group.id,
|
||||
pid: this.applicants[index].id,
|
||||
action: action
|
||||
})
|
||||
.then(res => {
|
||||
this.applicants.splice(index, 1);
|
||||
this.tab = 'review';
|
||||
this.$emit('decrementrc');
|
||||
if(action == 'approve') {
|
||||
this.$emit('incrementMemberCount');
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
backFromReview() {
|
||||
event.currentTarget.blur();
|
||||
this.memberSearchModel = null;
|
||||
this.tab = 'list';
|
||||
this.hideHeader = false;
|
||||
history.pushState(null, null, `/groups/${this.group.id}/members`);
|
||||
},
|
||||
|
||||
loadMoreApplicants() {
|
||||
this.loadingMoreApplicants = true;
|
||||
|
||||
axios.get('/api/v0/groups/members/requests', {
|
||||
params: {
|
||||
gid: this.group.id,
|
||||
page: this.applicantsPage
|
||||
}
|
||||
})
|
||||
.then(res => {
|
||||
this.applicants.push(...res.data);
|
||||
this.applicantsCanLoadMore = res.data.length == 10;
|
||||
this.applicantsPage++;
|
||||
this.loadingMoreApplicants = false;
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.group-members-component {
|
||||
min-height: 100vh;
|
||||
|
||||
.member-label {
|
||||
padding: 2px 5px;
|
||||
font-size: 12px;
|
||||
color: rgba(75, 119, 190, 1);
|
||||
background: rgba(137, 196, 244, 0.2);
|
||||
border: 1px solid rgba(137, 196, 244, 0.3);
|
||||
font-weight: 400;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.remote-label {
|
||||
padding: 2px 5px;
|
||||
font-size: 12px;
|
||||
color: #B45309;
|
||||
background: #FEF3C7;
|
||||
border: 1px solid #FCD34D;
|
||||
font-weight: 400;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
}
|
||||
</style>
|
231
resources/assets/components/groups/partials/GroupModeration.vue
Normal file
231
resources/assets/components/groups/partials/GroupModeration.vue
Normal file
|
@ -0,0 +1,231 @@
|
|||
<template>
|
||||
<div class="group-moderation-component">
|
||||
<div v-if="initalLoad">
|
||||
<div v-if="tab === 'home'">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-12 col-md-6 pt-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<p class="lead mb-0">Latest Mod Reports</p>
|
||||
<button class="btn btn-light border font-weight-bold btn-sm rounded shadow-sm">
|
||||
<i class="far fa-redo"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="reports.length" class="list-group">
|
||||
<div v-for="(report, index) in reports" class="list-group-item">
|
||||
<div class="media align-items-center">
|
||||
<img :src="report.profile.avatar" width="40" height="40" class="rounded-circle mr-3">
|
||||
<div class="media-body">
|
||||
<p class="mb-0">
|
||||
<a v-if="report.total_count == 1" class="font-weight-bold" :href="report.profile.url">{{ report.profile.username }}</a>
|
||||
<span v-else>
|
||||
<a class="font-weight-bold" :href="report.profile.url">{{ report.profile.username }}</a> and <a class="font-weight-bold" href="#">{{ report.total_count - 1}} others</a>
|
||||
</span>
|
||||
reported
|
||||
<a href="#" class="font-weight-bold" :id="`report_post:${index}`">this post</a>
|
||||
as {{ report.type }}
|
||||
</p>
|
||||
<p class="mb-0 small">
|
||||
<span class="text-muted font-weight-bold">
|
||||
{{ timeago(report.created_at) }}
|
||||
</span>
|
||||
<!-- <span>·</span>
|
||||
<a class="text-muted font-weight-bold" href="#">
|
||||
View Full Report
|
||||
</a> -->
|
||||
<span>·</span>
|
||||
<a class="text-danger font-weight-bold" href="#" @click.prevent="actionMenu(index)">
|
||||
Actions
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- <div class="text-muted">
|
||||
<button class="btn btn-light btn-sm shadow-sm" @click.prevent="actionMenu(index)">
|
||||
<i class="far fa-cog fa-lg text-lighter"></i>
|
||||
</button>
|
||||
</div> -->
|
||||
|
||||
<b-popover :target="`report_post:${index}`" triggers="hover" placement="bottom" custom-class="popover-wide">
|
||||
<template #title>
|
||||
<div class="d-flex justify-content-between">
|
||||
<span>
|
||||
@{{ report.status.account.username }}
|
||||
</span>
|
||||
<span>
|
||||
{{ timeago(report.status.created_at) }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div v-if="report.status.pf_type == 'group:post'">
|
||||
<div v-if="report.status.media_attachments.length">
|
||||
<img :src="report.status.media_attachments[0].url" width="100%" height="300" style="object-fit:cover;">
|
||||
</div>
|
||||
<div v-else>
|
||||
<p v-html="report.status.content"></p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="report.status.pf_type == 'reply-text'">
|
||||
<p v-html="report.status.content"></p>
|
||||
</div>
|
||||
<div v-else>
|
||||
<p>Cannot generate preview.</p>
|
||||
</div>
|
||||
|
||||
<p class="mb-1 mt-3">
|
||||
<a class="btn btn-primary btn-block font-weight-bold" :href="report.status.url">View Post</a>
|
||||
</p>
|
||||
</b-popover>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="canLoadMore" class="list-group-item">
|
||||
<button class="btn btn-light font-weight-bold btn-block" @click.prevent="loadMoreReports()">Load more</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="card card-body shadow-none border rounded-pill">
|
||||
<p class="lead font-weight-bold text-center mb-0">No moderation reports found!</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="col-12 col-md-6 pt-4 d-flex align-items-center justify-content-center">
|
||||
<div class="spinner-border" role="status">
|
||||
<span class="sr-only">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script type="text/javascript">
|
||||
export default {
|
||||
props: {
|
||||
group: {
|
||||
type: Object
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
initalLoad: false,
|
||||
reports: [],
|
||||
page: 1,
|
||||
canLoadMore: false,
|
||||
tab: 'home'
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.getReports();
|
||||
},
|
||||
|
||||
methods: {
|
||||
getReports() {
|
||||
axios.get(`/api/v0/groups/${this.group.id}/reports/list`)
|
||||
.then(res => {
|
||||
this.reports = res.data;
|
||||
this.initalLoad = true;
|
||||
this.page++;
|
||||
this.canLoadMore = res.data.length == 10;
|
||||
})
|
||||
},
|
||||
|
||||
loadMoreReports() {
|
||||
axios.get(`/api/v0/groups/${this.group.id}/reports/list`, {
|
||||
params: {
|
||||
page: this.page
|
||||
}
|
||||
})
|
||||
.then(res => {
|
||||
this.reports.push(...res.data);
|
||||
this.page++;
|
||||
this.canLoadMore = res.data.length == 10;
|
||||
})
|
||||
},
|
||||
|
||||
timeago(ts) {
|
||||
return App.util.format.timeAgo(ts);
|
||||
},
|
||||
|
||||
actionMenu(index) {
|
||||
event.currentTarget.blur();
|
||||
|
||||
swal({
|
||||
title: "Moderator Action",
|
||||
dangerMode: true,
|
||||
text: "Please select an action to take, press ESC to close",
|
||||
buttons: {
|
||||
ignore: {
|
||||
text: "Ignore",
|
||||
className: "btn-warning",
|
||||
value: "ignore"
|
||||
},
|
||||
cw: {
|
||||
text: "NSFW",
|
||||
className: "btn-warning",
|
||||
value: "cw",
|
||||
},
|
||||
delete: {
|
||||
text: "Delete",
|
||||
className: "btn-danger",
|
||||
value: "delete",
|
||||
},
|
||||
},
|
||||
})
|
||||
.then((value) => {
|
||||
switch (value) {
|
||||
|
||||
case "ignore":
|
||||
axios.post(`/api/v0/groups/${this.group.id}/report/action`, {
|
||||
action: 'ignore',
|
||||
id: this.reports[index].id
|
||||
}).then(res => {
|
||||
let report = this.reports[index];
|
||||
this.$emit('decrement', report.total_count);
|
||||
this.reports.splice(index, 1);
|
||||
this.$bvToast.toast(`Ignored and closed moderation report`, {
|
||||
title: 'Moderation Action',
|
||||
autoHideDelay: 5000,
|
||||
appendToast: true
|
||||
});
|
||||
})
|
||||
break;
|
||||
|
||||
case "cw":
|
||||
axios.post(`/api/v0/groups/${this.group.id}/report/action`, {
|
||||
action: 'cw',
|
||||
id: this.reports[index].id
|
||||
}).then(res => {
|
||||
let report = this.reports[index];
|
||||
this.$emit('decrement', report.total_count);
|
||||
this.reports.splice(index, 1);
|
||||
this.$bvToast.toast(`Succesfully applied content warning and closed moderation report`, {
|
||||
title: 'Moderation Action',
|
||||
autoHideDelay: 5000,
|
||||
appendToast: true
|
||||
});
|
||||
})
|
||||
break;
|
||||
|
||||
case "delete":
|
||||
swal('Oops, this is embarassing!', 'We have not implemented this moderation action yet.', 'error');
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.group-moderation-component {
|
||||
min-height: 80vh;
|
||||
margin-bottom: 100px;
|
||||
}
|
||||
|
||||
.popover-wide {
|
||||
min-width: 200px !important;
|
||||
}
|
||||
</style>
|
152
resources/assets/components/groups/partials/GroupPostModal.vue
Normal file
152
resources/assets/components/groups/partials/GroupPostModal.vue
Normal file
|
@ -0,0 +1,152 @@
|
|||
<template>
|
||||
<div class="group-post-modal">
|
||||
<b-modal
|
||||
ref="modal"
|
||||
size="xl"
|
||||
hide-footer
|
||||
hide-header
|
||||
centered
|
||||
body-class="gpm p-0">
|
||||
<div class="d-flex">
|
||||
<div class="gpm-media">
|
||||
<img :src="status.media_attachments[0].preview_url">
|
||||
</div>
|
||||
<div class="p-3" style="width: 30%;">
|
||||
<div class="media align-items-center mb-2">
|
||||
<a :href="status.account.url">
|
||||
<img class="rounded-circle media-avatar border mr-2" :src="status.account.avatar" width="32" height="32">
|
||||
</a>
|
||||
<div class="media-body">
|
||||
<div class="media-body-comment">
|
||||
<p class="media-body-comment-username mb-n1">
|
||||
<a :href="status.account.url" class="text-dark text-decoration-none font-weight-bold">
|
||||
{{status.account.acct}}
|
||||
</a>
|
||||
</p>
|
||||
<p class="media-body-comment-timestamp mb-0">
|
||||
<a class="font-weight-light text-muted small" :href="status.url">
|
||||
{{shortTimestamp(status.created_at)}}
|
||||
</a>
|
||||
<span class="text-lighter" style="padding-left:2px;padding-right:2px;">·</span>
|
||||
<span class="text-muted small"><i class="fas fa-globe"></i></span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-right" style="flex-grow:1;">
|
||||
<button class="btn btn-link text-dark py-0" type="button">
|
||||
<span class="fas fa-ellipsis-h text-lighter"></span>
|
||||
<span class="sr-only">Post Menu</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<read-more :status="status" />
|
||||
|
||||
<div class="border-top border-bottom mt-3">
|
||||
<div class="d-flex justify-content-between" style="padding: 8px 5px">
|
||||
<button class="btn btn-link py-0 text-decoration-none btn-sm" :class="{ 'font-weight-bold': status.favourited, 'text-primary': status.favourited, 'text-muted': !status.favourited }">
|
||||
<i class="far fa-heart mr-1"></i>
|
||||
{{ status.favourited ? 'Liked' : 'Like' }}
|
||||
</button>
|
||||
|
||||
<button class="btn btn-link py-0 text-decoration-none btn-sm text-muted">
|
||||
<i class="far fa-comment cursor-pointer text-muted mr-1"></i>
|
||||
Comment
|
||||
</button>
|
||||
|
||||
<button class="btn btn-link py-0 text-decoration-none btn-sm" disabled>
|
||||
<i class="fas fa-external-link-alt cursor-pointer text-muted mr-1"></i>
|
||||
Share
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- <comment-drawer
|
||||
:profile="profile"
|
||||
:status="status"
|
||||
:group-id="groupId" /> -->
|
||||
</div>
|
||||
</div>
|
||||
</b-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script type="text/javascript">
|
||||
import ReadMore from './ReadMore.vue';
|
||||
import CommentDrawer from './CommentDrawer.vue';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
groupId: {
|
||||
type: String
|
||||
},
|
||||
|
||||
status: {
|
||||
type: Object
|
||||
},
|
||||
|
||||
profile: {
|
||||
type: Object
|
||||
}
|
||||
},
|
||||
|
||||
components: {
|
||||
"read-more": ReadMore,
|
||||
"comment-drawer": CommentDrawer
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
loaded: false
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.init();
|
||||
},
|
||||
|
||||
methods: {
|
||||
init() {
|
||||
this.loaded = true;
|
||||
this.$refs.modal.show();
|
||||
},
|
||||
|
||||
shortTimestamp(ts) {
|
||||
return window.App.util.format.timeAgo(ts);
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.gpm {
|
||||
&-media {
|
||||
display: flex;
|
||||
width: 70%;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
max-height: 70vh;
|
||||
object-fit: contain;
|
||||
background-color: #000;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.comment-drawer-component {
|
||||
.my-3 {
|
||||
max-height: 46vh;
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.cdrawer-reply-form {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
margin-bottom: 1rem !important;
|
||||
min-width: 310px;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
199
resources/assets/components/groups/partials/GroupSearchModal.vue
Normal file
199
resources/assets/components/groups/partials/GroupSearchModal.vue
Normal file
|
@ -0,0 +1,199 @@
|
|||
<template>
|
||||
<div class="group-search-modal">
|
||||
<b-modal
|
||||
ref="modal"
|
||||
hide-header
|
||||
hide-footer
|
||||
centered
|
||||
rounded
|
||||
body-class="rounded group-search-modal-wrapper">
|
||||
<div class="d-flex justify-content-between">
|
||||
<autocomplete
|
||||
:search="autocompleteSearch"
|
||||
placeholder="Search this group"
|
||||
aria-label="Search this group"
|
||||
:get-result-value="getSearchResultValue"
|
||||
:debounceTime="700"
|
||||
@submit="onSearchSubmit"
|
||||
style="width: 100%;"
|
||||
ref="autocomplete"
|
||||
>
|
||||
<template #result="{ result, props }">
|
||||
<li
|
||||
v-bind="props"
|
||||
class="autocomplete-result"
|
||||
>
|
||||
<div class="text-truncate">
|
||||
<p class="result-name mb-0 font-weight-bold">
|
||||
{{ result.username }}
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
</template>
|
||||
</autocomplete>
|
||||
|
||||
<button class="btn btn-light border rounded-circle text-lighter ml-3" style="width: 52px;height:50px;" @click="close">
|
||||
<i class="fal fa-times fa-lg"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="recent && recent.length" class="pt-5">
|
||||
<h5 class="mb-2">Recent Searches</h5>
|
||||
<a v-for="(result, index) in recent" class="media align-items-center text-decoration-none text-dark" :href="result.action">
|
||||
<div class="bg-light rounded-circle mr-3 border d-flex justify-content-center align-items-center" style="width: 40px;height:40px">
|
||||
<i class="far fa-search"></i>
|
||||
</div>
|
||||
<div class="media-body">
|
||||
<p class="mb-0">{{ result.value }}</p>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="pt-5">
|
||||
<h5 class="mb-2">Explore This Group</h5>
|
||||
<div class="media align-items-center" @click="viewMyActivity">
|
||||
<img
|
||||
:src="profile?.avatar"
|
||||
width="40"
|
||||
height="40"
|
||||
class="mr-3 border rounded-circle"
|
||||
onerror="this.onerror=null;this.src='/storage/avatars/default.png?v=2'"
|
||||
/>
|
||||
<div class="media-body">
|
||||
<p class="mb-0">{{ profile?.display_name || profile?.username }}</p>
|
||||
<p class="mb-0 small text-muted">See your group activity.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="media align-items-center" @click="viewGroupSearch">
|
||||
<div class="bg-light rounded-circle mr-3 border d-flex justify-content-center align-items-center" style="width: 40px;height:40px">
|
||||
<i class="far fa-search"></i>
|
||||
</div>
|
||||
<div class="media-body">
|
||||
<p class="mb-0">Search all groups</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- <div class="text-center py-3 small">
|
||||
<p class="lead font-weight-normal mb-0">Looking for something?</p>
|
||||
<p class="mb-0">Search {{ group.name }} for posts, comments or members.</p>
|
||||
</div> -->
|
||||
</b-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script type="text/javascript">
|
||||
import Autocomplete from '@trevoreyre/autocomplete-vue'
|
||||
import '@trevoreyre/autocomplete-vue/dist/style.css'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
group: {
|
||||
type: Object
|
||||
},
|
||||
|
||||
profile: {
|
||||
type: Object
|
||||
}
|
||||
},
|
||||
|
||||
components: {
|
||||
'autocomplete': Autocomplete,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
query: '',
|
||||
recent: [],
|
||||
loaded: false
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
open() {
|
||||
this.fetchRecent();
|
||||
this.$refs.modal.show();
|
||||
},
|
||||
|
||||
close() {
|
||||
this.$refs.modal.hide();
|
||||
},
|
||||
|
||||
fetchRecent() {
|
||||
axios.get('/api/v0/groups/search/getrec', {
|
||||
params: {
|
||||
g: this.group.id
|
||||
}
|
||||
}).then(res => {
|
||||
this.recent = res.data;
|
||||
})
|
||||
},
|
||||
|
||||
autocompleteSearch(q) {
|
||||
if (!q || q.length < 2) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return axios.post(`/api/v0/groups/search/lac`, {
|
||||
q: q,
|
||||
g: this.group.id,
|
||||
v: '0.2'
|
||||
}).then(res => {
|
||||
return res.data;
|
||||
});
|
||||
return [];
|
||||
},
|
||||
|
||||
getSearchResultValue(result) {
|
||||
return result.username;
|
||||
},
|
||||
|
||||
onSearchSubmit(result) {
|
||||
if (result.length < 1) {
|
||||
return [];
|
||||
}
|
||||
|
||||
axios.post('/api/v0/groups/search/addrec', {
|
||||
g: this.group.id,
|
||||
q: {
|
||||
value: result.username,
|
||||
action: result.url
|
||||
}
|
||||
}).then(res => {
|
||||
location.href = result.url;
|
||||
});
|
||||
},
|
||||
|
||||
viewMyActivity() {
|
||||
location.href = `/groups/${this.group.id}/user/${this.profile.id}?rf=group_search`;
|
||||
},
|
||||
|
||||
viewGroupSearch() {
|
||||
location.href = `/groups/home?ct=gsearch&rf=group_search&rfid=${this.group.id}`;
|
||||
},
|
||||
|
||||
addToRecentSearches() {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.group-search-modal {
|
||||
|
||||
&-wrapper {
|
||||
.media {
|
||||
height: 60px;
|
||||
padding: 10px;
|
||||
border-radius: 10px;
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: #E5E7EB;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
870
resources/assets/components/groups/partials/GroupStatus.vue
Normal file
870
resources/assets/components/groups/partials/GroupStatus.vue
Normal file
|
@ -0,0 +1,870 @@
|
|||
<template>
|
||||
<div class="status-card-component" :class="{ 'status-card-sm': loaded && size === 'small' }">
|
||||
<div v-if="loaded" class="shadow-none mb-3">
|
||||
<div v-if="status.pf_type !== 'poll'" :class="{ 'border-top-0': !hasTopBorder }" class="card shadow-sm" style="border-radius: 18px !important;">
|
||||
<parent-unavailable
|
||||
v-if="parentUnavailable == true"
|
||||
:permalink-mode="permalinkMode"
|
||||
:permalink-status="childContext"
|
||||
:status="status"
|
||||
:profile="profile"
|
||||
:group-id="groupId"
|
||||
/>
|
||||
<div v-else class="card-body pb-0">
|
||||
<group-post-header
|
||||
:group="group"
|
||||
:status="status"
|
||||
:profile="profile"
|
||||
:showGroupHeader="showGroupHeader"
|
||||
:showGroupChevron="showGroupChevron"
|
||||
/>
|
||||
|
||||
<div>
|
||||
<div>
|
||||
<div class="pl-2">
|
||||
|
||||
<div v-if="status.sensitive && status.content.length" class="card card-body shadow-none border bg-light py-2 my-2 text-center user-select-none cursor-pointer" @click="status.sensitive = false">
|
||||
<div class="media justify-content-center align-items-center">
|
||||
<div class="mx-3">
|
||||
<i class="far fa-exclamation-triangle fa-2x text-lighter"></i>
|
||||
</div>
|
||||
<div class="media-body">
|
||||
<p class="font-weight-bold mb-0">Warning, may contain sensitive content. </p>
|
||||
<p class="mb-0 text-lighter small text-center font-weight-bold">Click to view</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<p v-html="renderedCaption" class="pt-2 text-break" style="font-size:15px;"></p>
|
||||
</template>
|
||||
|
||||
<photo-presenter
|
||||
v-if="status.pf_type === 'photo'"
|
||||
class="col px-0 border mb-4 rounded"
|
||||
:status="status"
|
||||
v-on:lightbox="showPostModal"
|
||||
v-on:togglecw="status.sensitive = false"
|
||||
@click="showPostModal"/>
|
||||
|
||||
<video-presenter
|
||||
v-else-if="status.pf_type === 'video'"
|
||||
class="col px-0 border mb-4 rounded"
|
||||
:status="status"
|
||||
v-on:togglecw="status.sensitive = false" />
|
||||
|
||||
<photo-album-presenter
|
||||
v-else-if="status.pf_type === 'photo:album'"
|
||||
class="col px-0 border mb-4 rounded"
|
||||
:status="status" v-on:lightbox="lightbox"
|
||||
v-on:togglecw="status.sensitive = false" />
|
||||
|
||||
<video-album-presenter
|
||||
v-else-if="status.pf_type === 'video:album'"
|
||||
class="col px-0 border mb-4 rounded"
|
||||
:status="status"
|
||||
v-on:togglecw="status.sensitive = false" />
|
||||
|
||||
<mixed-album-presenter
|
||||
v-else-if="status.pf_type === 'photo:video:album'"
|
||||
:status="status"
|
||||
class="col px-0 border mb-4 rounded"
|
||||
v-on:lightbox="lightbox"
|
||||
v-on:togglecw="status.sensitive = false" />
|
||||
|
||||
<div v-if="status.favourites_count || status.reply_count" class="border-top my-0">
|
||||
<div class="d-flex justify-content-between py-2" style="font-size: 14px;">
|
||||
<button v-if="status.favourites_count" class="btn btn-light py-0 text-decoration-none text-dark" style="font-size: 12px; font-weight: 600;" @click="showLikesModal($event)">
|
||||
{{ status.favourites_count }} {{ status.favourites_count == 1 ? 'Like' : 'Likes' }}
|
||||
</button>
|
||||
|
||||
<button v-if="status.reply_count" class="btn btn-light py-0 text-decoration-none text-dark" style="font-size: 12px; font-weight: 600;" @click="commentFocus($event)">
|
||||
{{ status.reply_count }} {{ status.reply_count == 1 ? 'Comment' : 'Comments' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="profile" class="border-top my-0">
|
||||
<div class="d-flex justify-content-between py-2 px-4">
|
||||
<!-- <button class="btn btn-link py-0 text-decoration-none" :class="{ 'font-weight-bold': status.favourited, 'text-primary': status.favourited, 'text-muted': !status.favourited }"
|
||||
@click="likeStatus(status, $event);">
|
||||
<i class="far fa-heart mr-1">
|
||||
</i>
|
||||
{{ status.favourited ? 'Liked' : 'Like' }}
|
||||
</button> -->
|
||||
<div>
|
||||
<button :id="'lr__'+status.id" class="btn btn-link py-0 text-decoration-none" :class="{ 'font-weight-bold': status.favourited, 'text-primary': status.favourited, 'text-muted': !status.favourited }" @click="likeStatus(status, $event);">
|
||||
<i class="far fa-heart mr-1"></i>
|
||||
{{ status.favourited ? 'Liked' : 'Like' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-link py-0 text-decoration-none text-muted" @click="commentFocus($event)">
|
||||
<i class="far fa-comment cursor-pointer text-muted mr-1">
|
||||
</i>
|
||||
Comment
|
||||
</button>
|
||||
|
||||
<button class="btn btn-link py-0 text-decoration-none" disabled>
|
||||
<i class="fas fa-external-link-alt cursor-pointer text-muted mr-1">
|
||||
</i>
|
||||
Share
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<comment-drawer
|
||||
v-if="showCommentDrawer"
|
||||
:permalink-mode="permalinkMode"
|
||||
:permalink-status="childContext"
|
||||
:status="status"
|
||||
:profile="profile"
|
||||
:group-id="groupId" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="border">
|
||||
<poll-card :status="status" :profile="profile" v-on:status-delete="statusDeleted" :showBorder="false" />
|
||||
|
||||
<div class="bg-white" style="padding: 0 1.25rem">
|
||||
<div v-if="profile" class="border-top my-0">
|
||||
<div class="d-flex justify-content-between py-2 px-4">
|
||||
<!-- <button class="btn btn-link py-0 text-decoration-none" :class="{ 'font-weight-bold': status.favourited, 'text-primary': status.favourited, 'text-muted': !status.favourited }" @click="likeStatus(status, $event);"> -->
|
||||
<div>
|
||||
<button :id="'lr__'+status.id" class="btn btn-link py-0 text-decoration-none" :class="{ 'font-weight-bold': status.favourited, 'text-primary': status.favourited, 'text-muted': !status.favourited }" @click="likeStatus(status, $event);">
|
||||
<i class="far fa-heart mr-1"></i>
|
||||
{{ status.favourited ? 'Liked' : 'Like' }}
|
||||
</button>
|
||||
|
||||
<b-popover :target="'lr__'+status.id" triggers="hover" placement="top">
|
||||
<template #title>Popover Title</template>
|
||||
I am popover <b>component</b> content!
|
||||
</b-popover>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-link py-0 text-decoration-none text-muted" @click="commentFocus($event)">
|
||||
<i class="far fa-comment cursor-pointer text-muted mr-1"></i>
|
||||
Comment
|
||||
</button>
|
||||
|
||||
<button class="btn btn-link py-0 text-decoration-none" disabled>
|
||||
<i class="fas fa-external-link-alt cursor-pointer text-muted mr-1">
|
||||
</i>
|
||||
Share
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<comment-drawer
|
||||
v-if="showCommentDrawer"
|
||||
:permalink-mode="permalinkMode"
|
||||
:permalink-status="childContext"
|
||||
:profile="profile"
|
||||
:status="status"
|
||||
:group-id="groupId" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- <div v-else class="card rounded-0 border-top-0 status-card card-md-rounded-0 shadow-none border">
|
||||
<div v-if="status" class="card-header d-inline-flex align-items-center bg-white">
|
||||
<div>
|
||||
<img class="rounded-circle box-shadow" :src="status.account.avatar" width="32px" height="32px" onerror="this.onerror=null;this.src='/storage/avatars/default.png?v=2'" alt="avatar">
|
||||
</div>
|
||||
<div class="pl-2">
|
||||
<a class="username font-weight-bold text-dark text-decoration-none text-break" v-bind:href="profileUrl(status)" v-html="statusCardUsernameFormat(status)">
|
||||
Loading...
|
||||
</a>
|
||||
<span v-if="status.account.is_admin" class="fa-stack" title="Admin Account" data-toggle="tooltip" style="height:1em; line-height:1em; max-width:19px;">
|
||||
<i class="fas fa-certificate text-danger fa-stack-1x"></i>
|
||||
<i class="fas fa-crown text-white fa-sm fa-stack-1x" style="font-size:7px;"></i>
|
||||
</span>
|
||||
<div class="d-flex align-items-center">
|
||||
<a v-if="status.place" class="small text-decoration-none text-muted" :href="'/discover/places/'+status.place.id+'/'+status.place.slug" title="Location" data-toggle="tooltip"><i class="fas fa-map-marked-alt"></i> {{status.place.name}}, {{status.place.country}}</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-right" style="flex-grow:1;">
|
||||
<button class="btn btn-link text-dark py-0" type="button" @click="ctxMenu()">
|
||||
<span class="fas fa-ellipsis-h text-lighter"></span>
|
||||
<span class="sr-only">Post Menu</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="postPresenterContainer" style="background: #000;">
|
||||
|
||||
<div v-if="status.pf_type === 'photo'" class="w-100">
|
||||
<photo-presenter
|
||||
:status="status"
|
||||
v-on:lightbox="lightbox"
|
||||
v-on:togglecw="status.sensitive = false"/>
|
||||
</div>
|
||||
|
||||
<div v-else-if="status.pf_type === 'video'" class="w-100">
|
||||
<video-presenter :status="status" v-on:togglecw="status.sensitive = false"></video-presenter>
|
||||
</div>
|
||||
|
||||
<div v-else-if="status.pf_type === 'photo:album'" class="w-100">
|
||||
<photo-album-presenter :status="status" v-on:lightbox="lightbox" v-on:togglecw="status.sensitive = false"></photo-album-presenter>
|
||||
</div>
|
||||
|
||||
<div v-else-if="status.pf_type === 'video:album'" class="w-100">
|
||||
<video-album-presenter :status="status" v-on:togglecw="status.sensitive = false"></video-album-presenter>
|
||||
</div>
|
||||
|
||||
<div v-else-if="status.pf_type === 'photo:video:album'" class="w-100">
|
||||
<mixed-album-presenter :status="status" v-on:lightbox="lightbox" v-on:togglecw="status.sensitive = false"></mixed-album-presenter>
|
||||
</div>
|
||||
|
||||
<div v-else class="w-100">
|
||||
<p class="text-center p-0 font-weight-bold text-white">Error: Problem rendering preview.</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div v-if="config.features.label.covid.enabled && status.label && status.label.covid == true" class="card-body border-top border-bottom py-2 cursor-pointer pr-2" @click="labelRedirect()">
|
||||
<p class="font-weight-bold d-flex justify-content-between align-items-center mb-0">
|
||||
<span>
|
||||
<i class="fas fa-info-circle mr-2"></i>
|
||||
For information about COVID-19, {{config.features.label.covid.org}}
|
||||
</span>
|
||||
<span>
|
||||
<i class="fas fa-chevron-right text-lighter"></i>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<div v-if="reactionBar" class="reactions my-1 pb-2">
|
||||
<h3 v-if="status.favourited" class="fas fa-heart text-danger pr-3 m-0 cursor-pointer" title="Like" v-on:click="likeStatus(status, $event);"></h3>
|
||||
<h3 v-else class="far fa-heart pr-3 m-0 like-btn text-dark cursor-pointer" title="Like" v-on:click="likeStatus(status, $event);"></h3>
|
||||
<h3 v-if="!status.comments_disabled" class="far fa-comment text-dark pr-3 m-0 cursor-pointer" title="Comment" v-on:click="commentFocus(status, $event)"></h3>
|
||||
<span v-if="status.taggedPeople.length" class="float-right">
|
||||
<span class="font-weight-light small" style="color:#718096">
|
||||
<i class="far fa-user" data-toggle="tooltip" title="Tagged People"></i>
|
||||
<span v-for="(tag, index) in status.taggedPeople" class="mr-n2">
|
||||
<a :href="'/'+tag.username">
|
||||
<img :src="tag.avatar" width="20px" height="20px" class="border rounded-circle" data-toggle="tooltip" :title="'@'+tag.username" alt="Avatar">
|
||||
</a>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div v-if="status.liked_by.username && status.liked_by.username !== profile.username" class="likes mb-1">
|
||||
<span class="like-count">Liked by
|
||||
<a class="font-weight-bold text-dark" :href="status.liked_by.url">{{status.liked_by.username}}</a>
|
||||
<span v-if="status.liked_by.others == true">
|
||||
and <span class="font-weight-bold" v-if="status.liked_by.total_count_pretty">{{status.liked_by.total_count_pretty}}</span> <span class="font-weight-bold">others</span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="status.pf_type != 'text'" class="caption">
|
||||
<p v-if="!status.sensitive" class="mb-2 read-more" style="overflow: hidden;">
|
||||
<span class="username font-weight-bold">
|
||||
<bdi><a class="text-dark" :href="profileUrl(status)">{{status.account.username}}</a></bdi>
|
||||
</span>
|
||||
<span class="status-content" v-html="status.content"></span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="timestamp mt-2">
|
||||
<p class="small mb-0">
|
||||
<a v-if="status.visibility != 'archived'" :href="statusUrl(status)" class="text-muted text-uppercase">
|
||||
<timeago :datetime="status.created_at" :auto-update="60" :converter-options="{includeSeconds:true}" :title="timestampFormat(status.created_at)" v-b-tooltip.hover.bottom></timeago>
|
||||
</a>
|
||||
<span v-else class="text-muted text-uppercase">
|
||||
Posted <timeago :datetime="status.created_at" :auto-update="60" :converter-options="{includeSeconds:true}" :title="timestampFormat(status.created_at)" v-b-tooltip.hover.bottom></timeago>
|
||||
</span>
|
||||
|
||||
<span v-if="recommended">
|
||||
<span class="px-1">·</span>
|
||||
<span class="text-muted">Based on popular and trending content</span>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<!-- <div v-else :class="{ 'border-top-0': !hasTopBorder }" class="card shadow-none border rounded-0">
|
||||
<div class="card-body pb-0">
|
||||
<div class="media">
|
||||
<img class="rounded-circle box-shadow mr-2" :src="status.account.avatar" width="42px" height="42px" onerror="this.onerror=null;this.src='/storage/avatars/default.png?v=2'" alt="avatar">
|
||||
<div class="media-body">
|
||||
<div class="pl-2 d-flex align-items-top">
|
||||
<div>
|
||||
<p class="mb-0">
|
||||
<a class="username font-weight-bold text-dark text-decoration-none text-break" v-bind:href="profileUrl(status)" v-html="statusCardUsernameFormat(status)">
|
||||
Loading...
|
||||
</a>
|
||||
</p>
|
||||
<p class="mb-0">
|
||||
<a class="font-weight-light text-muted small"
|
||||
:href="statusUrl(status)">{{shortTimestamp(status.created_at)}}</a>
|
||||
<span class="text-lighter" style="padding-left:2px;padding-right:2px;">·</span>
|
||||
<span class="text-muted small"><i class="fas fa-globe"></i></span>
|
||||
</p>
|
||||
</div>
|
||||
<span class="text-right" style="flex-grow:1;">
|
||||
<button class="btn btn-link text-dark py-0" type="button" @click="ctxMenu()">
|
||||
<span class="fas fa-ellipsis-h text-lighter"></span>
|
||||
<span class="sr-only">Post Menu</span>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div>
|
||||
<div class="pl-2">
|
||||
|
||||
<details v-if="status.sensitive">
|
||||
<summary class="mb-2 font-weight-bold text-muted">Content Warning</summary>
|
||||
<p v-html="status.content" class="pt-2 text-break status-content"></p>
|
||||
</details>
|
||||
|
||||
<p v-else v-html="status.content" class="pt-2 text-break" style="font-size:15px;"></p>
|
||||
|
||||
<div class="mb-1 row px-3">
|
||||
<photo-presenter
|
||||
v-if="status.pf_type === 'photo'"
|
||||
class="col px-0 border mb-4 rounded"
|
||||
:status="status"
|
||||
v-on:lightbox="lightbox"
|
||||
v-on:togglecw="status.sensitive = false"/>
|
||||
|
||||
<video-presenter
|
||||
v-else-if="status.pf_type === 'video'"
|
||||
class="col px-0 border mb-4 rounded"
|
||||
:status="status"
|
||||
v-on:togglecw="status.sensitive = false" />
|
||||
|
||||
<photo-album-presenter
|
||||
v-else-if="status.pf_type === 'photo:album'"
|
||||
class="col px-0 border mb-4 rounded"
|
||||
:status="status" v-on:lightbox="lightbox"
|
||||
v-on:togglecw="status.sensitive = false" />
|
||||
|
||||
<video-album-presenter
|
||||
v-else-if="status.pf_type === 'video:album'"
|
||||
class="col px-0 border mb-4 rounded"
|
||||
:status="status"
|
||||
v-on:togglecw="status.sensitive = false" />
|
||||
|
||||
<mixed-album-presenter
|
||||
v-else-if="status.pf_type === 'photo:video:album'"
|
||||
:status="status"
|
||||
class="col px-0 border mb-4 rounded"
|
||||
v-on:lightbox="lightbox"
|
||||
v-on:togglecw="status.sensitive = false" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="pl-2">
|
||||
|
||||
<div v-if="status.favourites_count || status.reply_count" class="border-top my-0">
|
||||
<div class="d-flex justify-content-between py-2" style="font-size: 14px;font-weight: 400;">
|
||||
<div v-if="status.favourites_count">
|
||||
{{ status.favourites_count }} Likes
|
||||
</div>
|
||||
|
||||
<button v-if="status.reply_count" class="btn btn-link py-0 text-decoration-none text-dark" @click="commentFocus($event)">
|
||||
{{ status.reply_count }} Comments
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="border-top my-0">
|
||||
<div class="d-flex justify-content-between py-2 px-4">
|
||||
<button class="btn btn-link py-0 text-decoration-none" :class="{ 'font-weight-bold': status.favourited, 'text-primary': status.favourited, 'text-muted': !status.favourited }"
|
||||
@click="likeStatus(status, $event);">
|
||||
<i class="far fa-heart mr-1">
|
||||
</i>
|
||||
{{ status.favourited ? 'Liked' : 'Like' }}
|
||||
</button>
|
||||
|
||||
<button class="btn btn-link py-0 text-decoration-none text-muted" @click="commentFocus($event)">
|
||||
<i class="far fa-comment cursor-pointer text-muted mr-1">
|
||||
</i>
|
||||
Comment
|
||||
</button>
|
||||
|
||||
<button class="btn btn-link py-0 text-decoration-none" disabled>
|
||||
<i class="fas fa-external-link-alt cursor-pointer text-muted mr-1">
|
||||
</i>
|
||||
Share
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<comment-drawer
|
||||
v-if="showCommentDrawer"
|
||||
:status="permalinkMode ? parentContext : status"
|
||||
:profile="profile"
|
||||
:permalink-mode="permalinkMode"
|
||||
:permalink-status="childContext"
|
||||
:group-id="groupId" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<context-menu
|
||||
v-if="profile"
|
||||
ref="contextMenu"
|
||||
:status="status"
|
||||
:profile="profile"
|
||||
:group-id="groupId"
|
||||
v-on:status-delete="statusDeleted"
|
||||
/>
|
||||
|
||||
<post-modal
|
||||
v-if="showModal"
|
||||
ref="modal"
|
||||
:status="status"
|
||||
:profile="profile"
|
||||
:groupId="groupId"
|
||||
/>
|
||||
|
||||
</div>
|
||||
|
||||
<div v-else class="card card-body shadow-none border mb-3" style="height: 200px;">
|
||||
<div class="w-100 h-100 d-flex justify-content-center align-items-center">
|
||||
<div class="spinner-border text-primary" role="status">
|
||||
<span class="sr-only">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- <b-modal
|
||||
v-if="likes && likes.length"
|
||||
ref="likeBox"
|
||||
title="Liked by"
|
||||
size="sm"
|
||||
centered
|
||||
hide-footer
|
||||
title="Likes"
|
||||
body-class="list-group-flush py-3 px-0">
|
||||
<div class="list-group">
|
||||
<div class="list-group-item border-0 py-1" v-for="(user, index) in likes" :key="'modal_likes_'+index+status.id">
|
||||
<div class="media">
|
||||
<a :href="user.url">
|
||||
<img class="mr-3 rounded-circle box-shadow" :src="user.avatar" :alt="user.username + '’s avatar'" width="30px" onerror="this.onerror=null;this.src='/storage/avatars/default.jpg';">
|
||||
</a>
|
||||
<div class="media-body">
|
||||
<p class="mb-0" style="font-size: 14px">
|
||||
<a :href="user.url" class="font-weight-bold text-dark">
|
||||
{{user.username}}
|
||||
</a>
|
||||
</p>
|
||||
<p v-if="!user.local" class="text-muted mb-0 text-truncate mr-3" style="font-size: 14px" :title="user.acct" data-toggle="dropdown" data-placement="bottom">
|
||||
<span class="font-weight-bold">{{user.acct.split('@')[0]}}</span><span class="text-lighter">@{{user.acct.split('@')[1]}}</span>
|
||||
</p>
|
||||
<p v-else class="text-muted mb-0 text-truncate" style="font-size: 14px">
|
||||
{{user.display_name}}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<infinite-loading v-if="likes && likes.length" @infinite="infiniteLikesHandler" spinner="spiral">
|
||||
<div slot="no-more"></div>
|
||||
<div slot="no-results"></div>
|
||||
</infinite-loading>
|
||||
</div>
|
||||
</b-modal> -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script type="text/javascript">
|
||||
import CommentDrawer from './CommentDrawer.vue';
|
||||
import ContextMenu from './ContextMenu.vue';
|
||||
import PollCard from '~/partials/PollCard.vue';
|
||||
import MixedAlbumPresenter from '@/presenter/MixedAlbumPresenter.vue';
|
||||
import PhotoAlbumPresenter from '@/presenter/PhotoAlbumPresenter.vue';
|
||||
import PhotoPresenter from '@/presenter/PhotoPresenter.vue';
|
||||
import VideoAlbumPresenter from '@/presenter/VideoAlbumPresenter.vue';
|
||||
import VideoPresenter from '@/presenter/VideoPresenter.vue';
|
||||
import GroupPostModal from './GroupPostModal.vue';
|
||||
import { autoLink } from 'twitter-text';
|
||||
import ParentUnavailable from '@/groups/partials/Status/ParentUnavailable.vue';
|
||||
import GroupPostHeader from '@/groups/partials/Status/GroupHeader.vue';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
groupId: {
|
||||
type: String
|
||||
},
|
||||
|
||||
group: {
|
||||
type: Object
|
||||
},
|
||||
|
||||
profile: {
|
||||
type: Object
|
||||
},
|
||||
|
||||
prestatus: {
|
||||
type: Object
|
||||
},
|
||||
|
||||
recommended: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
|
||||
reactionBar: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
|
||||
hasTopBorder: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
|
||||
size: {
|
||||
type: String,
|
||||
validator: (val) => ['regular', 'small'].includes(val),
|
||||
default: 'regular'
|
||||
},
|
||||
|
||||
permalinkMode: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
|
||||
showGroupChevron: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
|
||||
showGroupHeader: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
|
||||
components: {
|
||||
"comment-drawer": CommentDrawer,
|
||||
"context-menu": ContextMenu,
|
||||
"poll-card": PollCard,
|
||||
"mixed-album-presenter": MixedAlbumPresenter,
|
||||
"photo-album-presenter": PhotoAlbumPresenter,
|
||||
"photo-presenter": PhotoPresenter,
|
||||
"video-album-presenter": VideoAlbumPresenter,
|
||||
"video-presenter": VideoPresenter,
|
||||
"post-modal": GroupPostModal,
|
||||
"parent-unavailable": ParentUnavailable,
|
||||
"group-post-header": GroupPostHeader
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
config: window.App.config,
|
||||
status: {},
|
||||
loaded: false,
|
||||
replies: [],
|
||||
replyId: null,
|
||||
lightboxMedia: false,
|
||||
showSuggestions: true,
|
||||
showReadMore: true,
|
||||
replyStatus: {},
|
||||
replyText: '',
|
||||
replyNsfw: false,
|
||||
emoji: window.App.util.emoji,
|
||||
commentDrawerKey: 0,
|
||||
showCommentDrawer: false,
|
||||
parentContext: undefined,
|
||||
childContext: undefined,
|
||||
parentUnavailable: undefined,
|
||||
showModal: false,
|
||||
likes: [],
|
||||
likesPage: 1,
|
||||
openLikesModal: false,
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
renderedCaption: {
|
||||
get() {
|
||||
if(this.prestatus) {
|
||||
const gid = this.prestatus.gid;
|
||||
return autoLink(
|
||||
this.prestatus.content,
|
||||
{
|
||||
hashtagUrlBase: App.config.site.url + `/groups/${gid}/topics/`,
|
||||
usernameUrlBase: App.config.site.url + `/groups/${gid}/username/`
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
return this.prestatus.content;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.status = this.prestatus;
|
||||
let self = this;
|
||||
setTimeout(() => {
|
||||
if(this.permalinkMode == true && this.prestatus.in_reply_to_id) {
|
||||
self.childContext = self.status;
|
||||
axios.get('/api/v0/groups/status', {
|
||||
params: {
|
||||
gid: self.groupId,
|
||||
sid: self.status.in_reply_to_id
|
||||
}
|
||||
}).then(res => {
|
||||
self.status = res.data;
|
||||
self.parentUnavailable = false;
|
||||
self.showCommentDrawer = true;
|
||||
self.parentContext = res.data;
|
||||
self.loaded = true;
|
||||
}).catch(err => {
|
||||
self.status = this.prestatus;
|
||||
self.parentUnavailable = true;
|
||||
self.showCommentDrawer = true;
|
||||
self.parentContext = this.prestatus;
|
||||
self.loaded = true;
|
||||
});
|
||||
} else {
|
||||
self.parentUnavailable = false;
|
||||
self.showCommentDrawer = false;
|
||||
self.loaded = true;
|
||||
}
|
||||
}, 100);
|
||||
},
|
||||
|
||||
methods: {
|
||||
formatCount(count) {
|
||||
return App.util.format.count(count);
|
||||
},
|
||||
|
||||
statusUrl(status) {
|
||||
if(status.local == true) {
|
||||
return status.url;
|
||||
}
|
||||
|
||||
return '/i/web/post/_/' + status.account.id + '/' + status.id;
|
||||
},
|
||||
|
||||
profileUrl(status) {
|
||||
if(status.local == true) {
|
||||
return status.account.url;
|
||||
}
|
||||
|
||||
return '/i/web/profile/_/' + status.account.id;
|
||||
},
|
||||
|
||||
timestampFormat(timestamp) {
|
||||
let ts = new Date(timestamp);
|
||||
return ts.toDateString() + ' ' + ts.toLocaleTimeString();
|
||||
},
|
||||
|
||||
shortTimestamp(ts) {
|
||||
return window.App.util.format.timeAgo(ts);
|
||||
},
|
||||
|
||||
statusCardUsernameFormat(status) {
|
||||
if(status.account.local == true) {
|
||||
return status.account.username;
|
||||
}
|
||||
|
||||
let fmt = window.App.config.username.remote.format;
|
||||
let txt = window.App.config.username.remote.custom;
|
||||
let usr = status.account.username;
|
||||
let dom = document.createElement('a');
|
||||
dom.href = status.account.url;
|
||||
dom = dom.hostname;
|
||||
|
||||
switch(fmt) {
|
||||
case '@':
|
||||
return usr + '<span class="text-lighter font-weight-bold">@' + dom + '</span>';
|
||||
break;
|
||||
|
||||
case 'from':
|
||||
return usr + '<span class="text-lighter font-weight-bold"> <span class="font-weight-normal">from</span> ' + dom + '</span>';
|
||||
break;
|
||||
|
||||
case 'custom':
|
||||
return usr + '<span class="text-lighter font-weight-bold"> ' + txt + ' ' + dom + '</span>';
|
||||
break;
|
||||
|
||||
default:
|
||||
return usr + '<span class="text-lighter font-weight-bold">@' + dom + '</span>';
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
lightbox(status) {
|
||||
window.location.href = status.media_attachments[0].url;
|
||||
},
|
||||
|
||||
labelRedirect(type) {
|
||||
let url = '/i/redirect?url=' + encodeURI(this.config.features.label.covid.url);
|
||||
window.location.href = url;
|
||||
},
|
||||
|
||||
likeStatus(status, event) {
|
||||
event.currentTarget.blur();
|
||||
let count = status.favourites_count;
|
||||
let state = status.favourited ? 'unlike' : 'like';
|
||||
axios.post('/api/v0/groups/status/' + state, {
|
||||
sid: status.id,
|
||||
gid: this.groupId
|
||||
}).then(res => {
|
||||
status.favourited = state;
|
||||
status.favourites_count = state? count + 1 : count - 1;
|
||||
status.favourited = state;
|
||||
if(status.favourited) {
|
||||
setTimeout(function() {
|
||||
event.target.classList.add('animate__animated', 'animate__bounce');
|
||||
},100);
|
||||
}
|
||||
}).catch(err => {
|
||||
if(err.response.status == 422) {
|
||||
swal('Error', err.response.data.error, 'error');
|
||||
} else {
|
||||
swal('Error', 'Something went wrong, please try again later.', 'error');
|
||||
}
|
||||
});
|
||||
window.navigator.vibrate(200);
|
||||
},
|
||||
|
||||
commentFocus($event) {
|
||||
$event.target.blur();
|
||||
this.showCommentDrawer = !this.showCommentDrawer;
|
||||
},
|
||||
|
||||
commentSubmit(status, $event) {
|
||||
this.replySending = true;
|
||||
let id = status.id;
|
||||
let comment = this.replyText;
|
||||
let limit = this.config.uploader.max_caption_length;
|
||||
if(comment.length > limit) {
|
||||
this.replySending = false;
|
||||
swal('Comment Too Long', 'Please make sure your comment is '+limit+' characters or less.', 'error');
|
||||
return;
|
||||
}
|
||||
axios.post('/i/comment', {
|
||||
item: id,
|
||||
comment: comment,
|
||||
sensitive: this.replyNsfw
|
||||
}).then(res => {
|
||||
this.replyText = '';
|
||||
this.replies.push(res.data.entity);
|
||||
this.$refs.replyModal.hide();
|
||||
});
|
||||
this.replySending = false;
|
||||
},
|
||||
|
||||
owner(status) {
|
||||
return this.profile.id === status.account.id;
|
||||
},
|
||||
|
||||
admin() {
|
||||
return this.profile.is_admin == true;
|
||||
},
|
||||
|
||||
ownerOrAdmin(status) {
|
||||
return this.owner(status) || this.admin();
|
||||
},
|
||||
|
||||
ctxMenu() {
|
||||
this.$refs.contextMenu.open();
|
||||
},
|
||||
|
||||
timeAgo(ts) {
|
||||
return App.util.format.timeAgo(ts);
|
||||
},
|
||||
|
||||
statusDeleted(status) {
|
||||
this.$emit('status-delete');
|
||||
},
|
||||
|
||||
showPostModal() {
|
||||
this.showModal = true;
|
||||
this.$refs.modal.init();
|
||||
},
|
||||
|
||||
showLikesModal(event) {
|
||||
if(event && event.hasOwnProperty('currentTarget')) {
|
||||
event.currentTarget().blur();
|
||||
}
|
||||
|
||||
this.$emit('likes-modal');
|
||||
return;
|
||||
|
||||
if(this.likes.length) {
|
||||
this.$refs.likeBox.show();
|
||||
return;
|
||||
}
|
||||
|
||||
axios.get('/api/v0/groups/'+this.groupId+'/likes/'+this.status.id)
|
||||
.then(res => {
|
||||
this.likes = res.data.data;
|
||||
this.$refs.likeBox.show();
|
||||
});
|
||||
},
|
||||
|
||||
infiniteLikesHandler($state) {
|
||||
axios.get('/api/v0/groups/'+this.groupId+'/likes/'+this.status.id, {
|
||||
params: {
|
||||
page: this.likesPage,
|
||||
},
|
||||
}).then(({ data }) => {
|
||||
if (data.data.length > 0) {
|
||||
this.likes.push(...data.data);
|
||||
this.likesPage++;
|
||||
$state.loaded();
|
||||
} else {
|
||||
$state.complete();
|
||||
}
|
||||
});
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.status-card-component {
|
||||
.status-content {
|
||||
font-size: 17px;
|
||||
}
|
||||
|
||||
&.status-card-sm {
|
||||
.status-content {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.fa-lg {
|
||||
font-size: unset;
|
||||
line-height: unset;
|
||||
vertical-align: unset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.reaction-bar {
|
||||
width: auto;
|
||||
max-width: unset;
|
||||
left: -50px !important;
|
||||
border: 1px solid #F3F4F6 !important;
|
||||
|
||||
.popover-body {
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
display: none;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 48px;
|
||||
}
|
||||
}
|
||||
</style>
|
73
resources/assets/components/groups/partials/GroupTopics.vue
Normal file
73
resources/assets/components/groups/partials/GroupTopics.vue
Normal file
|
@ -0,0 +1,73 @@
|
|||
<template>
|
||||
<div class="group-topics-component">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-12 col-md-8">
|
||||
<div class="card card-body border shadow-sm">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<p class="h4 font-weight-bold mb-0">Group Topics</p>
|
||||
<select class="form-control bg-light rounded-lg border font-weight-bold py-2" style="width:95px;" disabled>
|
||||
<option>All</option>
|
||||
<option>Pinned</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div v-if="feed.length">
|
||||
<div v-for="(tag, index) in feed" class="">
|
||||
<div class="media py-2">
|
||||
<i class="fas fa-hashtag fa-lg text-lighter mr-3 mt-2"></i>
|
||||
<div :class="{ 'border-bottom': index != feed.length - 1 }" class="media-body">
|
||||
<a :href="tag.url" class="font-weight-bold mb-1 text-dark" style="font-size: 16px;">
|
||||
{{ tag.name }}
|
||||
</a>
|
||||
<p style="font-size: 13px;" class="text-muted">{{ tag.count }} posts in this group</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="py-5">
|
||||
<p class="lead text-center font-weight-bold">No topics found</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script type="text/javascript">
|
||||
export default {
|
||||
props: {
|
||||
group: {
|
||||
type: Object
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
feed: []
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.fetchTopics();
|
||||
},
|
||||
|
||||
methods: {
|
||||
fetchTopics() {
|
||||
axios.get('/api/v0/groups/topics/list', {
|
||||
params: {
|
||||
gid: this.group.id
|
||||
}
|
||||
}).then(res => {
|
||||
this.feed = res.data;
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.group-topics-component {
|
||||
margin-bottom: 50vh;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,9 @@
|
|||
<template>
|
||||
|
||||
</template>
|
||||
|
||||
<script type="text/javascript">
|
||||
export default {
|
||||
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,172 @@
|
|||
<template>
|
||||
<div>
|
||||
<b-modal
|
||||
v-if="profile && profile.hasOwnProperty('avatar')"
|
||||
ref="home"
|
||||
hide-footer
|
||||
centered
|
||||
rounded
|
||||
title="Limit Interactions"
|
||||
body-class="rounded">
|
||||
|
||||
<div class="media mb-3">
|
||||
<a :href="profile.url" class="text-dark text-decoration-none">
|
||||
<img :src="profile.avatar" width="56" height="56" class="rounded-circle border mr-2" />
|
||||
</a>
|
||||
|
||||
<div class="media-body">
|
||||
<p class="lead font-weight-bold mb-0">
|
||||
<a :href="profile.url" class="text-dark text-decoration-none">{{profile.username}}</a>
|
||||
<span v-if="profile.role == 'founder'" class="member-label rounded ml-1">Admin</span>
|
||||
</p>
|
||||
<p class="text-muted mb-0">Member since {{formatDate(profile.joined)}}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="w-100 bg-light mb-1 font-weight-bold d-flex justify-content-center align-items-center border rounded" style="min-height:240px;">
|
||||
<div v-if="limitsLoaded" class="py-3">
|
||||
<p class="lead mb-0">Interaction Permissions</p>
|
||||
<p class="small text-muted">Last updated: {{ updated ? formatDate(updated) : 'Never' }}</p>
|
||||
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" v-model="limits.can_post" :disabled="savingChanges">
|
||||
<label class="form-check-label">
|
||||
Can create posts
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" v-model="limits.can_comment" :disabled="savingChanges">
|
||||
<label class="form-check-label">
|
||||
Can create comments
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" v-model="limits.can_like" :disabled="savingChanges">
|
||||
<label class="form-check-label">
|
||||
Can like posts and comments
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<button class="btn btn-primary font-weight-bold float-right" @click.prevent="saveChanges" :disabled="savingChanges" style="width:130px;">
|
||||
<b-spinner v-if="savingChanges" variant="light" small />
|
||||
<span v-else>Save changes</span>
|
||||
</button>
|
||||
</div>
|
||||
<div v-else class="d-flex align-items-center flex-column">
|
||||
<b-spinner variant="muted" />
|
||||
<p class="pt-3 small text-muted font-weight-bold">Loading interaction limits...</p>
|
||||
</div>
|
||||
</div>
|
||||
</b-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script type="text/javascript">
|
||||
export default {
|
||||
props: {
|
||||
group: {
|
||||
type: Object
|
||||
},
|
||||
profile: {
|
||||
type: Object
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
limitsLoaded: false,
|
||||
limits: {
|
||||
can_post: true,
|
||||
can_comment: true,
|
||||
can_like: true
|
||||
},
|
||||
updated: null,
|
||||
savingChanges: false
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
fetchInteractionLimits() {
|
||||
axios.get(`/api/v0/groups/${this.group.id}/members/interaction-limits`, {
|
||||
params: {
|
||||
profile_id: this.profile.id
|
||||
}
|
||||
})
|
||||
.then(res => {
|
||||
this.limits = res.data.limits;
|
||||
this.updated = res.data.updated_at;
|
||||
this.limitsLoaded = true;
|
||||
}).catch(err => {
|
||||
this.$refs.home.hide();
|
||||
swal('Oops!', 'Cannot fetch interaction limits at this time, please try again later.', 'error');
|
||||
})
|
||||
},
|
||||
|
||||
open(profile) {
|
||||
this.loaded = true;
|
||||
this.$refs.home.show();
|
||||
this.fetchInteractionLimits();
|
||||
},
|
||||
|
||||
formatDate(ts) {
|
||||
return new Date(ts).toDateString();
|
||||
},
|
||||
|
||||
saveChanges() {
|
||||
event.currentTarget.blur();
|
||||
this.savingChanges = true;
|
||||
|
||||
axios.post(`/api/v0/groups/${this.group.id}/members/interaction-limits`, {
|
||||
profile_id: this.profile.id,
|
||||
can_post: this.limits.can_post,
|
||||
can_comment: this.limits.can_comment,
|
||||
can_like: this.limits.can_like,
|
||||
})
|
||||
.then(res => {
|
||||
this.savingChanges = false;
|
||||
this.$refs.home.hide();
|
||||
|
||||
this.$bvToast.toast(`Updated interaction limits for ${this.profile.username}`, {
|
||||
title: 'Success',
|
||||
variant: 'success',
|
||||
autoHideDelay: 5000
|
||||
});
|
||||
}).catch(err => {
|
||||
this.savingChanges = false;
|
||||
this.$refs.home.hide();
|
||||
|
||||
if(err.response.status == 422 && err.response.data.error == 'limit_reached') {
|
||||
swal('Limit Reached', 'You cannot add any more member limitations', 'info');
|
||||
// swal({
|
||||
// title: 'Limit Reached',
|
||||
// text: 'You cannot add any more member limitations',
|
||||
// icon: 'info',
|
||||
// buttons: {
|
||||
// info: {
|
||||
// className: 'btn-light border',
|
||||
// text: 'More info',
|
||||
// value: 'more-info'
|
||||
// },
|
||||
|
||||
// ok: {
|
||||
// text: 'Ok',
|
||||
// value: null
|
||||
// },
|
||||
// }
|
||||
// }).then(value => {
|
||||
// if(value == 'more-info') {
|
||||
// location.href = '/site/kb/groups/interaction-limits';
|
||||
// }
|
||||
// });
|
||||
} else {
|
||||
swal('Oops!', 'An error occured while processing this request, please try again later.', 'error');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,38 @@
|
|||
<template>
|
||||
<div class="member-only-warning">
|
||||
<div class="member-only-warning-wrapper">
|
||||
<h3>Content unavailable</h3>
|
||||
<p>You need to join this Group before you can access this content.</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.member-only-warning {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
&-wrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
max-width: 550px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--border-color);
|
||||
padding: 4rem 1rem;
|
||||
|
||||
|
||||
h3 {
|
||||
font-weight: bold;
|
||||
letter-spacing: -1px;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 1.2em;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,44 @@
|
|||
<template>
|
||||
<div class="px-md-5" style="background-color: #fff;">
|
||||
<img
|
||||
v-if="group.metadata && group.metadata.hasOwnProperty('header')"
|
||||
:src="group.metadata.header.url"
|
||||
class="header-image">
|
||||
<div v-else class="header-jumbotron"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
group: {
|
||||
type: Object
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.header-image {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
object-fit: cover;
|
||||
max-height: 220px;
|
||||
border-bottom-left-radius: 5px;
|
||||
border-bottom-right-radius: 5px;
|
||||
margin-top:-1px;
|
||||
border: 1px solid var(--light);
|
||||
margin-bottom: 0px;
|
||||
|
||||
@media(min-width: 768px) {
|
||||
max-height: 420px;
|
||||
}
|
||||
}
|
||||
|
||||
.header-jumbotron {
|
||||
background-color: #F3F4F6;
|
||||
height: 320px;
|
||||
border-bottom-left-radius: 20px;
|
||||
border-bottom-right-radius: 20px;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,199 @@
|
|||
<template>
|
||||
<div class="col-12 group-feed-component-header px-3 px-md-5">
|
||||
<div class="media align-items-end">
|
||||
<img
|
||||
v-if="group.metadata && group.metadata.hasOwnProperty('avatar')"
|
||||
:src="group.metadata.avatar.url"
|
||||
width="169"
|
||||
height="169"
|
||||
class="bg-white mx-4 rounded-circle border shadow p-1"
|
||||
style="object-fit: cover;"
|
||||
:style="{ 'margin-top': group.metadata && group.metadata.hasOwnProperty('header') && group.metadata.header.url ? '-100px' : '0' }"
|
||||
/>
|
||||
|
||||
<div v-if="!group || !group.name" class="media-body">
|
||||
<h3 class="d-flex align-items-start">
|
||||
<span>Loading...</span>
|
||||
</h3>
|
||||
</div>
|
||||
<div v-else class="media-body px-3">
|
||||
<h3 class="d-flex align-items-start">
|
||||
<span>{{ group.name.slice(0,118) }}</span>
|
||||
<sup v-if="group.verified" class="fa-stack ml-n2" title="Verified Group" data-toggle="tooltip">
|
||||
<i class="fas fa-circle fa-stack-1x fa-lg" style="color: #22a7f0cc;font-size:18px"></i>
|
||||
<i class="fas fa-check fa-stack-1x text-white" style="font-size:10px"></i>
|
||||
</sup>
|
||||
</h3>
|
||||
<p class="text-muted mb-0" style="font-weight: 300;">
|
||||
<span>
|
||||
<i class="fas fa-globe mr-1"></i>
|
||||
{{ group.membership == 'all' ? 'Public Group' : 'Private Group'}}
|
||||
</span>
|
||||
<span class="mx-2">
|
||||
·
|
||||
</span>
|
||||
<span>{{ group.member_count == 1 ? group.member_count + ' Member' : group.member_count + ' Members' }}</span>
|
||||
<span class="mx-2">
|
||||
·
|
||||
</span>
|
||||
<span v-if="group.local" class="rounded member-label">Local</span>
|
||||
<span v-else class="rounded remote-label">Remote</span>
|
||||
<span v-if="group.self && group.self.hasOwnProperty('role') && group.self.role">
|
||||
<span class="mx-2">
|
||||
·
|
||||
</span>
|
||||
<span class="rounded member-label">{{ group.self.role }}</span>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="group && group.self">
|
||||
<button v-if="!isMember && !group.self.is_requested" class="btn btn-primary cta-btn font-weight-bold" @click="joinGroup" :disabled="requestingMembership">
|
||||
<span v-if="!requestingMembership">
|
||||
{{ group.membership == 'all' ? 'Join' : 'Request Membership' }}
|
||||
</span>
|
||||
<div
|
||||
v-else
|
||||
class="spinner-border spinner-border-sm"
|
||||
role="status">
|
||||
<span class="sr-only">Loading...</span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
v-else-if="!isMember && group.self.is_requested"
|
||||
class="btn btn-light border cta-btn font-weight-bold"
|
||||
@click.prevent="cancelJoinRequest">
|
||||
<i class="fas fa-user-clock mr-1"></i> Requested to Join
|
||||
</button>
|
||||
|
||||
<button
|
||||
v-else-if="!isAdmin && isMember && !group.self.is_requested"
|
||||
type="button"
|
||||
class="btn btn-light border cta-btn font-weight-bold"
|
||||
@click.prevent="leaveGroup">
|
||||
<i class="fas sign-out-alt mr-1"></i> Leave Group
|
||||
</button>
|
||||
|
||||
<!-- <div v-if="isAdmin">
|
||||
<a
|
||||
class="btn btn-light border cta-btn font-weight-bold"
|
||||
:href="group.url + '/settings'">
|
||||
Settings
|
||||
</a>
|
||||
</div> -->
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
group: {
|
||||
type: Object
|
||||
},
|
||||
isAdmin: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
isMember: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
requestingMembership: false,
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
joinGroup() {
|
||||
this.requestingMembership = true;
|
||||
|
||||
axios.post('/api/v0/groups/'+this.group.id+'/join')
|
||||
.then(res => {
|
||||
this.requestingMembership = false;
|
||||
this.$emit('refresh');
|
||||
}).catch(err => {
|
||||
let body = err.response;
|
||||
|
||||
if(body.status == 422) {
|
||||
this.requestingMembership = false;
|
||||
swal('Oops!', body.data.error, 'error');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
cancelJoinRequest() {
|
||||
if(!window.confirm('Are you sure you want to cancel your request to join this group?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
axios.post('/api/v0/groups/'+this.group.id+'/cjr')
|
||||
.then(res => {
|
||||
this.requestingMembership = false;
|
||||
this.$emit('refresh');
|
||||
}).catch(err => {
|
||||
let body = err.response;
|
||||
|
||||
if(body.status == 422) {
|
||||
swal('Oops!', body.data.error, 'error');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
leaveGroup() {
|
||||
if(!window.confirm('Are you sure you want to leave this group? Any content you shared will remain accessible. You won\'t be able to rejoin for 24 hours.')) {
|
||||
return;
|
||||
}
|
||||
|
||||
axios.post('/api/v0/groups/'+this.group.id+'/leave')
|
||||
.then(res => {
|
||||
this.$emit('refresh');
|
||||
});
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.group-feed-component {
|
||||
&-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-end;
|
||||
padding: 1rem 0;
|
||||
background-color: transparent;
|
||||
|
||||
.cta-btn {
|
||||
width: 190px;
|
||||
}
|
||||
}
|
||||
|
||||
.member-label {
|
||||
padding: 2px 5px;
|
||||
font-size: 12px;
|
||||
color: rgba(75, 119, 190, 1);
|
||||
background:rgba(137, 196, 244, 0.2);
|
||||
border:1px solid rgba(137, 196, 244, 0.3);
|
||||
font-weight:400;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.remote-label {
|
||||
padding: 2px 5px;
|
||||
font-size: 12px;
|
||||
color: #B45309;
|
||||
background: #FEF3C7;
|
||||
border: 1px solid #FCD34D;
|
||||
font-weight: 400;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,167 @@
|
|||
<template>
|
||||
<div>
|
||||
<div class="col-12 border-top group-feed-component-menu px-5">
|
||||
<ul class="nav font-weight-bold group-feed-component-menu-nav">
|
||||
<li class="nav-item">
|
||||
<router-link :to="`/groups/${group.id}/about`" class="nav-link">About</router-link>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<router-link :to="`/groups/${group.id}`" class="nav-link" exact>Feed</router-link>
|
||||
</li>
|
||||
<li v-if="group?.self && group.self.is_member" class="nav-item">
|
||||
<router-link :to="`/groups/${group.id}/topics`" class="nav-link">Topics</router-link>
|
||||
</li>
|
||||
<li v-if="group?.self && group.self.is_member" class="nav-item">
|
||||
<router-link :to="`/groups/${group.id}/members`" class="nav-link">
|
||||
Members
|
||||
<span v-if="group.self.is_member && isAdmin && atabs.request_count" class="badge badge-danger rounded-pill ml-2" style="height: 20px;padding:4px 8px;font-size: 11px;">{{atabs.request_count}}</span>
|
||||
</router-link>
|
||||
</li>
|
||||
<!-- <li v-if="group.self.is_member" class="nav-item">
|
||||
<a :class="{active: tab == 'events'}" class="nav-link" href="#" @click.prevent="switchTab('events')">Events</a>
|
||||
</li> -->
|
||||
<li v-if="group?.self && group.self.is_member" class="nav-item">
|
||||
<router-link :to="`/groups/${group.id}/media`" class="nav-link">Media</router-link>
|
||||
</li>
|
||||
<!-- <li v-if="group.self.is_member" class="nav-item">
|
||||
<a class="nav-link" href="#">Popular</a>
|
||||
</li> -->
|
||||
<!-- <li v-if="group.self.is_member" class="nav-item">
|
||||
<a :class="{active: tab == 'polls'}" class="nav-link" href="#" @click.prevent="switchTab('polls')">Polls</a>
|
||||
</li> -->
|
||||
|
||||
<!-- <li v-if="group.self.is_member && isAdmin" class="nav-item">
|
||||
<a class="nav-link" href="#">Messages</a>
|
||||
</li> -->
|
||||
|
||||
<!-- <li v-if="group.self.is_member && isAdmin" class="nav-item">
|
||||
<a :class="{active: tab == 'insights'}" class="nav-link" href="#" @click.prevent="switchTab('insights')">Insights</a>
|
||||
</li> -->
|
||||
<!-- <li v-if="group.self.is_member && isAdmin && group.membership != 'all'" class="nav-item">
|
||||
<a :class="{active: tab == 'requests'}" class="nav-link" href="#" @click.prevent="switchTab('requests')">
|
||||
<span class="mr-2">
|
||||
<i class="far fa-user-plus mr-1"></i>
|
||||
Requests
|
||||
</span>
|
||||
<span v-if="atabs.request_count" class="badge badge-danger rounded-pill" style="height: 20px;padding:4px 8px;font-size: 11px;">{{atabs.request_count}}</span>
|
||||
<span v-if="atabs.request_count" class="badge badge-danger rounded-pill" style="height: 20px;padding:4px 8px;font-size: 11px;">99+</span>
|
||||
</a>
|
||||
</li> -->
|
||||
<li v-if="group?.self && group.self.is_member && isAdmin" class="nav-item">
|
||||
<router-link :to="`/groups/${group.id}/moderation`" class="nav-link d-flex align-items-top">
|
||||
<span class="mr-2">Moderation</span>
|
||||
<span v-if="atabs.moderation_count" class="badge badge-danger rounded-pill" style="height: 20px;padding:4px 8px;font-size: 11px;">{{atabs.moderation_count}}</span>
|
||||
</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
<div>
|
||||
<button
|
||||
v-if="group?.self && group.self.is_member"
|
||||
class="btn btn-light btn-sm border px-3 rounded-pill mr-2"
|
||||
@click="showSearchModal">
|
||||
<i class="far fa-search"></i>
|
||||
</button>
|
||||
<div class="dropdown d-inline">
|
||||
<button class="btn btn-light btn-sm border px-3 rounded-pill dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<i class="far fa-cog"></i>
|
||||
</button>
|
||||
<div class="dropdown-menu dropdown-menu-right">
|
||||
<a class="dropdown-item" href="#" @click.prevent="copyLink">
|
||||
Copy Group Link
|
||||
</a>
|
||||
|
||||
<a class="dropdown-item" href="#" @click.prevent="showInviteModal">
|
||||
Invite friends
|
||||
</a>
|
||||
|
||||
<a v-if="!isAdmin" class="dropdown-item" href="#" @click.prevent="reportGroup">
|
||||
Report Group
|
||||
</a>
|
||||
|
||||
<a v-if="isAdmin" class="dropdown-item" :href="group.url + '/settings'">
|
||||
Settings
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<search-modal
|
||||
ref="searchModal"
|
||||
:group="group"
|
||||
:profile="profile"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import SearchModal from '@/groups/partials/GroupSearchModal.vue';
|
||||
export default {
|
||||
props: {
|
||||
group: {
|
||||
type: Object
|
||||
},
|
||||
isAdmin: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
isMember: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
atabs: {
|
||||
type: Object
|
||||
},
|
||||
profile: {
|
||||
type: Object
|
||||
}
|
||||
},
|
||||
|
||||
components: {
|
||||
'search-modal': SearchModal,
|
||||
},
|
||||
|
||||
methods: {
|
||||
showSearchModal() {
|
||||
event.currentTarget.blur();
|
||||
this.$refs.searchModal.open();
|
||||
},
|
||||
|
||||
// showInviteModal() {
|
||||
// event.currentTarget.blur();
|
||||
// this.$refs.inviteModal.open();
|
||||
// },
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.group-feed-component {
|
||||
&-menu {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0;
|
||||
|
||||
&-nav {
|
||||
.nav-item {
|
||||
.nav-link {
|
||||
padding-top: 1rem;
|
||||
padding-bottom: 1rem;
|
||||
color: #6c757d;
|
||||
|
||||
&.active {
|
||||
color: #2c78bf;
|
||||
border-bottom: 2px solid #2c78bf;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:not(last-child) {
|
||||
.nav-item {
|
||||
margin-right: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
51
resources/assets/components/groups/partials/ReadMore.vue
Normal file
51
resources/assets/components/groups/partials/ReadMore.vue
Normal file
|
@ -0,0 +1,51 @@
|
|||
<template>
|
||||
<div class="read-more-component" style="word-break: break-all;">
|
||||
<div v-if="status.content.length < 200" v-html="content"></div>
|
||||
<div v-else>
|
||||
<span v-html="content"></span>
|
||||
<a
|
||||
v-if="cursor == 200 || fullContent.length > cursor"
|
||||
class="font-weight-bold text-muted" href="#"
|
||||
style="display: block;white-space: nowrap;"
|
||||
@click.prevent="readMore">
|
||||
<i class="d-none fas fa-caret-down"></i> Read more...
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script type="text/javascript">
|
||||
export default {
|
||||
props: {
|
||||
status: {
|
||||
type: Object
|
||||
},
|
||||
|
||||
cursorLimit: {
|
||||
type: Number,
|
||||
default: 200
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
fullContent: null,
|
||||
content: null,
|
||||
cursor: 200
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.cursor = this.cursorLimit;
|
||||
this.fullContent = this.status.content;
|
||||
this.content = this.status.content.substr(0, this.cursor);
|
||||
},
|
||||
|
||||
methods: {
|
||||
readMore() {
|
||||
this.cursor = this.cursor + 200;
|
||||
this.content = this.fullContent.substr(0, this.cursor);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
465
resources/assets/components/groups/partials/SelfDiscover.vue
Normal file
465
resources/assets/components/groups/partials/SelfDiscover.vue
Normal file
|
@ -0,0 +1,465 @@
|
|||
<template>
|
||||
<div class="self-discover-component col-12 col-md-9 bg-lighter border-left mb-4">
|
||||
<div class="px-5">
|
||||
<div class="jumbotron my-4 text-light bg-mantle">
|
||||
<div class="container">
|
||||
<h1 class="display-4">Discover</h1>
|
||||
<p class="lead mb-0">Explore group communities and topics</p>
|
||||
<!-- <p class="lead">
|
||||
<button class="btn btn-outline-light">Browse Categories</button>
|
||||
</p> -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="tab === 'home'" class="px-5">
|
||||
<div class="row mb-4 pt-5">
|
||||
<div class="col-12 col-md-4">
|
||||
<h4 class="font-weight-bold">Popular</h4>
|
||||
<div class="list-group list-group-scroll">
|
||||
<a v-for="(group, index) in popularGroups"
|
||||
class="list-group-item p-1"
|
||||
:href="group.url">
|
||||
<group-list-card :group="group" :compact="true" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-4">
|
||||
<div class="card card-body shadow-none bg-mantle text-light" style="margin-top: 33px;">
|
||||
<h3 class="mb-4 font-weight-lighter">Discover communities and topics based on your interests</h3>
|
||||
<p class="mb-0">
|
||||
<button class="btn btn-outline-light font-weight-light btn-block" @click="toggleTab('categories')">Browse Categories</button>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="card card-body shadow-none bg-light text-dark border" style="margin-top: 20px;">
|
||||
<p class="lead mb-4 text-muted font-weight-lighter mb-1">Browse Public Groups</p>
|
||||
<!-- <p class="lead mb-4 text-muted font-weight-lighter">Tips for growing your group membership</p> -->
|
||||
<!-- <h5 class="mb-4 text-muted font-weight-lighter">Create and easily organize events</h5> -->
|
||||
<p class="mb-0">
|
||||
<button class="btn btn-light border font-weight-light btn-block">Group Directory</button>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-4">
|
||||
<h4 class="font-weight-bold">New</h4>
|
||||
<div class="list-group list-group-scroll">
|
||||
<a v-for="(group, index) in newGroups"
|
||||
class="list-group-item p-1"
|
||||
:href="group.url">
|
||||
<group-list-card :group="group" :compact="true" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="jumbotron mb-4 text-light bg-black" style="margin-top: 5rem;">
|
||||
<div class="container">
|
||||
<h1 class="display-4">Across the Fediverse</h1>
|
||||
<p class="mb-0">
|
||||
<button
|
||||
class="btn btn-outline-light"
|
||||
@click="toggleTab('fediverseGroups')"
|
||||
>
|
||||
Explore fediverse groups <i class="fal fa-chevron-right ml-2"></i>
|
||||
</button>
|
||||
</p>
|
||||
<hr class="my-4">
|
||||
<p class="lead">We're in the early stages of Group federation, and working with other projects to support cross-platform compatibility. <a href="#">Learn more about group federation <i class="fal fa-chevron-right ml-2 fa-sm"></i></a></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row my-4 py-5">
|
||||
<div class="col-12 col-md-4">
|
||||
<div class="card card-body shadow-none bg-light" style="border:1px solid #E5E7EB;">
|
||||
<p class="text-center text-lighter">
|
||||
<i class="fal fa-lightbulb fa-4x"></i>
|
||||
</p>
|
||||
<p class="text-center lead mb-0">What's New</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-4">
|
||||
<div class="card card-body shadow-none bg-light" style="border:1px solid #E5E7EB;">
|
||||
<p class="text-center text-lighter">
|
||||
<i class="fal fa-clipboard-list-check fa-4x"></i>
|
||||
</p>
|
||||
<p class="text-center lead mb-0">User Guide</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-4">
|
||||
<div class="card card-body shadow-none bg-light" style="border:1px solid #E5E7EB;">
|
||||
<p class="text-center text-lighter">
|
||||
<i class="fal fa-question-circle fa-4x"></i>
|
||||
</p>
|
||||
<p class="text-center lead mb-0">Groups Help</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="text-lighter" style="font-size:9px">
|
||||
<span class="font-weight-bold mr-1">Groups v0.0.1</span>
|
||||
</p>
|
||||
|
||||
<!-- <div class="my-4 pt-5">
|
||||
<p class="h4 font-weight-bold mb-1">Suggested for You</p>
|
||||
<p class="lead text-muted mb-0">Groups you might be interested in</p>
|
||||
</div>
|
||||
|
||||
<div class="row mb-4">
|
||||
<div v-for="(group, index) in recommended.slice(recommendedStart, recommendedEnd)" :key="'rec:'+group.id+':'+index" class="col-12 col-md-4 slide-fade">
|
||||
<div class="card shadow-sm border text-decoration-none text-dark">
|
||||
<img v-if="group.metadata && group.metadata.hasOwnProperty('header')" :src="group.metadata.header.url" class="card-img-top" style="width: 100%; height: auto;object-fit: cover;max-height: 160px;">
|
||||
<div v-else class="bg-primary" style="width:100%;height:160px;"></div>
|
||||
<div class="card-body">
|
||||
<div class="lead font-weight-bold d-flex align-items-top" style="height: 60px;">
|
||||
{{ group.name }}
|
||||
<span v-if="group.verified" class="fa-stack ml-n2 mt-n2">
|
||||
<i class="fas fa-circle fa-stack-1x fa-lg" style="color: #22a7f0cc;font-size:18px"></i>
|
||||
<i class="fas fa-check fa-stack-1x text-white" style="font-size:10px"></i>
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-muted font-weight-light d-flex justify-content-between">
|
||||
<span>{{group.member_count}} Members</span>
|
||||
</div>
|
||||
<hr>
|
||||
<p class="mb-0">
|
||||
<a class="btn btn-light btn-block border rounded-lg font-weight-bold" :href="group.url">View Group</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="index == 0 && recommended.length > 3 && recommendedStart != 0" style="position: absolute; top: 45%; left: 0px;transform:translateY(-55%);">
|
||||
<button class="btn btn-light shadow-lg btn-lg rounded-circle border d-flex align-items-center justify-content-center" style="width: 50px;height:50px;" @click="recommendedPrev()">
|
||||
<i class="fas fa-chevron-left fa-lg"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="index == 2 && recommended.length > 3" style="position: absolute; top: 45%; right: 0px;transform:translateY(-55%);">
|
||||
<button class="btn btn-light shadow-lg btn-lg rounded-circle border d-flex align-items-center justify-content-center" style="width: 50px;height:50px;" @click="recommendedNext()">
|
||||
<i class="fas fa-chevron-right fa-lg"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="py-2 mb-2">
|
||||
<hr>
|
||||
</div> -->
|
||||
|
||||
<!-- <div class="px-4 pb-4">
|
||||
<p class="h4 font-weight-bold mb-1">Friends' Groups</p>
|
||||
<p class="lead text-muted mb-0">Groups your mutuals are in.</p>
|
||||
</div> -->
|
||||
|
||||
<!-- <div class="px-4 py-2">
|
||||
<hr>
|
||||
</div> -->
|
||||
|
||||
<!-- <div class="pb-4">
|
||||
<p class="h4 font-weight-bold mb-1">Categories</p>
|
||||
<p class="lead text-muted mb-0">Find a group by browsing top categories</p>
|
||||
</div>
|
||||
|
||||
<div class="row mb-4">
|
||||
<div v-for="(group, index) in categories.slice(categoriesStart, categoriesEnd)" :key="'rec:'+group.id+':'+index" class="col-12 col-md-2 slide-fade">
|
||||
<div class="card card-body rounded-lg shadow-sm border text-decoration-none bg-primary p-2 text-white d-flex justify-content-end" style="width: 150px; height:150px; background: linear-gradient(45deg, #ff512f, #dd2476);">
|
||||
<p class="mb-0 font-weight-bold" style="font-size:15px">{{group}}</p>
|
||||
</div>
|
||||
<div v-if="index == 0 && categories.length > 3 && categoriesStart != 0" style="position: absolute; top: 50%; left: -10px;transform:translateY(-50%);">
|
||||
<button class="btn btn-light shadow-lg btn-lg rounded-circle border d-flex align-items-center justify-content-center" style="width: 50px;height:50px;" @click="categoriesPrev()">
|
||||
<i class="fas fa-chevron-left fa-lg"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="index == 5 && categories.length > 3" style="position: absolute; top: 50%; right: -10px;transform:translateY(-50%);">
|
||||
<button class="btn btn-light shadow-lg btn-lg rounded-circle border d-flex align-items-center justify-content-center" style="width: 50px;height:50px;" @click="categoriesNext()">
|
||||
<i class="fas fa-chevron-right fa-lg"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<!-- <div class="py-2 mb-2">
|
||||
<hr>
|
||||
</div> -->
|
||||
|
||||
<!-- <div class="pb-4 my-4">
|
||||
<p class="h4 font-weight-bold mb-1">My Groups</p>
|
||||
<p class="lead text-muted mb-0">Groups you are a member of</p>
|
||||
</div>
|
||||
|
||||
<div class="row mb-4">
|
||||
<div v-for="(group, index) in selfGroups.slice(selfGroupsStart, selfGroupsEnd)" :key="'rec:'+group.id+':'+index" class="col-12 col-md-4 slide-fade">
|
||||
<div class="card shadow-sm border text-decoration-none text-dark">
|
||||
<img v-if="group.metadata && group.metadata.hasOwnProperty('header')" :src="group.metadata.header.url" class="card-img-top" style="width: 100%; height: auto;object-fit: cover;max-height: 160px;">
|
||||
<div v-else class="bg-primary" style="width:100%;height:160px;"></div>
|
||||
<div class="card-body">
|
||||
<div class="lead font-weight-bold d-flex align-items-top" style="height: 60px;">
|
||||
{{ group.name }}
|
||||
<span v-if="group.verified" class="fa-stack ml-n2 mt-n2">
|
||||
<i class="fas fa-circle fa-stack-1x fa-lg" style="color: #22a7f0cc;font-size:18px"></i>
|
||||
<i class="fas fa-check fa-stack-1x text-white" style="font-size:10px"></i>
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-muted font-weight-light d-flex justify-content-between">
|
||||
<span>{{group.member_count}} Members</span>
|
||||
</div>
|
||||
<hr>
|
||||
<p class="mb-0">
|
||||
<a class="btn btn-light btn-block border rounded-lg font-weight-bold" :href="group.url">View Group</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="index == 0 && selfGroups.length > 3 && selfGroupsStart != 0" style="position: absolute; top: 50%; left: -10px;transform:translateY(-50%);">
|
||||
<button class="btn btn-light shadow-lg btn-lg rounded-circle border d-flex align-items-center justify-content-center" style="width: 50px;height:50px;" @click="selfGroupsPrev()">
|
||||
<i class="fas fa-chevron-left fa-lg"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="index == 2 && selfGroups.length > 3" style="position: absolute; top: 50%; right: -10px;transform:translateY(-50%);">
|
||||
<button class="btn btn-light shadow-lg btn-lg rounded-circle border d-flex align-items-center justify-content-center" style="width: 50px;height:50px;" @click="selfGroupsNext()">
|
||||
<i class="fas fa-chevron-right fa-lg"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
</div>
|
||||
|
||||
<div v-if="tab === 'categories'" class="px-5">
|
||||
<div class="row my-4 justify-content-center">
|
||||
<div class="col-12 col-md-6">
|
||||
<div class="title mb-4">
|
||||
<span>Categories</span>
|
||||
<button class="btn btn-light font-weight-bold" @click="toggleTab('home')">Go Back</button>
|
||||
</div>
|
||||
|
||||
<div class="list-group">
|
||||
<div
|
||||
v-for="(group, index) in categories"
|
||||
:key="'rec:'+group.id+':'+index"
|
||||
class="list-group-item"
|
||||
@click="selectCategory(index)">
|
||||
<p class="mb-0 font-weight-bold">
|
||||
{{group}}
|
||||
<span class="float-right">
|
||||
<i class="fal fa-chevron-right"></i>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="tab === 'category'" class="px-5">
|
||||
<div class="row my-4 justify-content-center">
|
||||
<div class="col-12 col-md-6">
|
||||
<div class="title mb-4">
|
||||
<div>
|
||||
<div class="mb-n2 small text-uppercase text-lighter">Categories</div>
|
||||
<span>{{ categories[activeCategoryIndex] }}</span>
|
||||
</div>
|
||||
<button class="btn btn-light font-weight-bold" @click="toggleTab('categories')">Go Back</button>
|
||||
</div>
|
||||
|
||||
<div v-if="categoryGroupsLoaded">
|
||||
<div class="list-group">
|
||||
<a v-for="(group, index) in categoryGroups"
|
||||
class="list-group-item p-1"
|
||||
:href="group.url">
|
||||
<group-list-card :group="group" :showStats="true" />
|
||||
</a>
|
||||
|
||||
<div
|
||||
v-if="categoryGroupsCanLoadMore"
|
||||
class="list-group-item">
|
||||
|
||||
<button
|
||||
class="btn btn-light font-weight-bold btn-block"
|
||||
@click="fetchCategoryGroups">
|
||||
Load more
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="categoryGroups.length === 0" class="mt-3">
|
||||
<div class="bg-white border text-center p-3">
|
||||
<p class="font-weight-light mb-0">No groups found in this category</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<div class="card card-body shadow-none border justify-content-center flex-row">
|
||||
<b-spinner />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="tab === 'fediverseGroups'" class="px-5">
|
||||
<div class="row my-4 justify-content-center">
|
||||
<div class="col-12 col-md-6">
|
||||
<div class="title mb-4">
|
||||
<span>Fediverse Groups</span>
|
||||
<button class="btn btn-light font-weight-bold" @click="toggleTab('home')">Go Back</button>
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
<div class="bg-white border text-center p-3">
|
||||
<p class="font-weight-light mb-0">No fediverse groups found</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script type="text/javascript">
|
||||
import GroupListCard from './GroupListCard.vue';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
profile: {
|
||||
type: Object
|
||||
}
|
||||
},
|
||||
|
||||
components: {
|
||||
"group-list-card": GroupListCard
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
isLoaded: false,
|
||||
tab: 'home',
|
||||
popularGroups: [],
|
||||
newGroups: [],
|
||||
activeCategoryIndex: undefined,
|
||||
activeCategoryPage: 1,
|
||||
categories: [],
|
||||
categoriesStart: 0,
|
||||
categoriesEnd: 6,
|
||||
categoryGroups: [],
|
||||
categoryGroupsLoaded: false,
|
||||
categoryGroupsCanLoadMore: false,
|
||||
// selfGroups: [],
|
||||
// selfGroupsStart: 0,
|
||||
// selfGroupsEnd: 3
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.fetchPopular();
|
||||
this.fetchCategories();
|
||||
// this.fetchSelf();
|
||||
},
|
||||
|
||||
methods: {
|
||||
fetchPopular() {
|
||||
axios.get('/api/v0/groups/discover/popular')
|
||||
.then(res => {
|
||||
this.popularGroups = res.data;
|
||||
this.fetchNew();
|
||||
})
|
||||
},
|
||||
|
||||
fetchNew() {
|
||||
axios.get('/api/v0/groups/discover/new')
|
||||
.then(res => {
|
||||
this.newGroups = res.data;
|
||||
})
|
||||
},
|
||||
|
||||
fetchCategories() {
|
||||
axios.get('/api/v0/groups/categories/list')
|
||||
.then(res => {
|
||||
this.categories = res.data;
|
||||
})
|
||||
},
|
||||
|
||||
toggleTab(tab) {
|
||||
window.scrollTo(0, 0);
|
||||
this.tab = tab;
|
||||
},
|
||||
|
||||
selectCategory(index) {
|
||||
window.scrollTo(0, 0);
|
||||
if(index !== this.activeCategoryIndex) {
|
||||
this.activeCategoryPage = 1;
|
||||
}
|
||||
this.activeCategoryIndex = index;
|
||||
this.fetchCategoryGroups();
|
||||
},
|
||||
|
||||
fetchCategoryGroups() {
|
||||
if(this.activeCategoryPage == 1) {
|
||||
this.categoryGroupsLoaded = false;
|
||||
}
|
||||
|
||||
axios.get('/api/v0/groups/category/list', {
|
||||
params: {
|
||||
name: this.categories[this.activeCategoryIndex],
|
||||
page: this.activeCategoryPage
|
||||
}
|
||||
})
|
||||
.then(res => {
|
||||
this.tab = 'category';
|
||||
if(this.activeCategoryPage == 1) {
|
||||
this.categoryGroups = res.data;
|
||||
} else {
|
||||
this.categoryGroups.push(...res.data);
|
||||
}
|
||||
if(res.data.length == 6) {
|
||||
this.categoryGroupsCanLoadMore = true;
|
||||
this.activeCategoryPage++;
|
||||
} else {
|
||||
this.categoryGroupsCanLoadMore = false;
|
||||
}
|
||||
setTimeout(() => {
|
||||
this.categoryGroupsLoaded = true;
|
||||
}, 600);
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.self-discover-component {
|
||||
.list-group-item {
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
background-color: #F3F4F6;
|
||||
}
|
||||
}
|
||||
|
||||
.bg-mantle {
|
||||
background: linear-gradient(45deg, #24c6dc, #514a9d);
|
||||
}
|
||||
|
||||
.bg-black {
|
||||
background-color: #000;
|
||||
|
||||
hr {
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.12);
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
span {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.btn {
|
||||
border: 1px solid #E5E7EB;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
146
resources/assets/components/groups/partials/SelfFeed.vue
Normal file
146
resources/assets/components/groups/partials/SelfFeed.vue
Normal file
|
@ -0,0 +1,146 @@
|
|||
<template>
|
||||
<div class="col-12 col-md-9" style="overflow:hidden">
|
||||
<div class="row h-100 bg-light justify-content-center">
|
||||
<div class="col-12 col-md-10 col-lg-7">
|
||||
<div v-if="!initalLoad">
|
||||
<p class="text-center mt-5 pt-5 font-weight-bold">Loading...</p>
|
||||
</div>
|
||||
<div v-else class="px-5">
|
||||
<div v-if="emptyFeed">
|
||||
<div class="jumbotron mt-5">
|
||||
<h1 class="display-4">Hello 👋</h1>
|
||||
<h3 class="font-weight-light">Welcome to Pixelfed Groups!</h3>
|
||||
<p class="lead">Groups are a way to participate in like minded communities and topics.</p>
|
||||
<hr class="my-4">
|
||||
<p>Anyone can create and manage their own group as long as it abides by our <a href="#">community guidelines</a>.</p>
|
||||
<p class="text-center mb-0">
|
||||
<router-link to="/groups/discover" class="btn btn-primary btn-lg rounded-pill">
|
||||
Discover Groups
|
||||
</router-link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<div class="my-4">
|
||||
<p class="h1 font-weight-bold mb-1">Groups Feed</p>
|
||||
<p class="lead text-muted mb-0">Recent posts from your groups</p>
|
||||
</div>
|
||||
|
||||
<div class="my-3">
|
||||
<group-status
|
||||
v-for="(status, index) in feed"
|
||||
:key="'gs:' + status.id + index"
|
||||
:prestatus="status"
|
||||
:profile="profile"
|
||||
:show-group-header="true"
|
||||
:group="status.group"
|
||||
:group-id="status.group.id" />
|
||||
|
||||
<div v-if="feed.length > 2">
|
||||
<infinite-loading @infinite="infiniteFeed" :distance="800">
|
||||
<div slot="no-more" class="my-3">
|
||||
<p class="lead font-weight-bold pt-5">You have reached the end of this feed</p>
|
||||
<div style="height: 10rem;"></div>
|
||||
</div>
|
||||
<div slot="no-results"></div>
|
||||
</infinite-loading>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <div class="col-12 col-md-4">
|
||||
<div class="mt-5 media align-items-center">
|
||||
<div class="mr-3">
|
||||
<i class="fas fa-info-circle fa-2x text-lighter"></i>
|
||||
</div>
|
||||
<p class="media-body text-muted mb-0 font-weight-light">Groups are in beta, some <a href="#" class="font-weight-bold">limitations</a> apply.</p>
|
||||
</div>
|
||||
</div> -->
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script type="text/javascript">
|
||||
import GroupStatus from './GroupStatus.vue';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
profile: {
|
||||
type: Object
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
feed: [],
|
||||
ids: [],
|
||||
page: 1,
|
||||
tab: 'feed',
|
||||
initalLoad: false,
|
||||
emptyFeed: true
|
||||
};
|
||||
},
|
||||
|
||||
components: {
|
||||
'group-status': GroupStatus
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.fetchFeed();
|
||||
},
|
||||
|
||||
methods: {
|
||||
fetchFeed() {
|
||||
axios.get('/api/v0/groups/self/feed', {
|
||||
params: {
|
||||
initial: true
|
||||
}
|
||||
})
|
||||
.then(res => {
|
||||
this.page++;
|
||||
this.feed = res.data;
|
||||
this.emptyFeed = this.feed.length === 0;
|
||||
this.initalLoad = true;
|
||||
})
|
||||
},
|
||||
|
||||
infiniteFeed($state) {
|
||||
if(this.feed.length < 2 || this.page > 5) {
|
||||
$state.complete();
|
||||
return;
|
||||
}
|
||||
|
||||
axios.get('/api/v0/groups/self/feed', {
|
||||
params: {
|
||||
page: this.page
|
||||
},
|
||||
}).then(res => {
|
||||
if (res.data.length) {
|
||||
let data = res.data;
|
||||
let self = this;
|
||||
data.forEach(d => {
|
||||
if(self.ids.indexOf(d.id) == -1) {
|
||||
self.ids.push(d.id);
|
||||
self.feed.push(d);
|
||||
}
|
||||
});
|
||||
$state.loaded();
|
||||
this.page++;
|
||||
} else {
|
||||
$state.complete();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
switchTab(tab) {
|
||||
this.tab = tab;
|
||||
},
|
||||
|
||||
gotoDiscover() {
|
||||
this.$emit('switchtab', 'discover');
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
171
resources/assets/components/groups/partials/SelfGroups.vue
Normal file
171
resources/assets/components/groups/partials/SelfGroups.vue
Normal file
|
@ -0,0 +1,171 @@
|
|||
<template>
|
||||
<div class="my-groups-component">
|
||||
<div class="list-container">
|
||||
<div v-if="isLoaded">
|
||||
<div class="list-group">
|
||||
<a
|
||||
v-for="(group, index) in groups" :key="'rec:'+group.id+':'+index"
|
||||
class="list-group-item text-decoration-none"
|
||||
:href="group.url">
|
||||
<group-list-card
|
||||
:group="group"
|
||||
:truncateDescriptionLength="140"
|
||||
:showStats="true" />
|
||||
<!-- <div class="media align-items-center">
|
||||
<img v-if="group.metadata && group.metadata.hasOwnProperty('header')" :src="group.metadata.header.url" class="mr-3 border rounded" style="width: 100px; height: 60px;object-fit: cover;padding:5px;">
|
||||
|
||||
<div v-else class="mr-3 border rounded" style="width: 100px; height: 60px;padding: 5px;">
|
||||
<div class="bg-primary d-flex align-items-center justify-content-center" style="width: 100%; height:100%;">
|
||||
<i class="fal fa-users text-white fa-lg"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="media-body">
|
||||
<p class="h5 font-weight-bold mb-1 text-dark">
|
||||
{{ group.name || 'Untitled Group' }}
|
||||
</p>
|
||||
|
||||
<p class="text-muted small mb-1 read-more">
|
||||
{{ truncate(group.description) }}
|
||||
</p>
|
||||
|
||||
<p class="mb-0">
|
||||
<span class="text-muted mr-2">
|
||||
<i class="far fa-users"></i>
|
||||
<strong class="small">{{ prettyCount(group.member_count) }}</strong>
|
||||
</span>
|
||||
|
||||
<span class="member-label">
|
||||
{{ group.self.role }}
|
||||
</span>
|
||||
|
||||
<span v-if="!group.local" class="remote-label ml-2">
|
||||
<i class="fal fa-globe"></i> Remote
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div> -->
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<p v-if="canLoadMore">
|
||||
<button
|
||||
class="btn btn-primary btn-block font-weight-bold mt-3"
|
||||
@click.prevent="loadMore"
|
||||
:disabled="loadingMore">
|
||||
Load more
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-else class="d-flex justify-content-center">
|
||||
<b-spinner/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script type="text/javascript">
|
||||
import GroupListCard from './GroupListCard.vue';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
profile: {
|
||||
type: Object
|
||||
}
|
||||
},
|
||||
|
||||
components: {
|
||||
"group-list-card": GroupListCard
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
isLoaded: false,
|
||||
groups: [],
|
||||
canLoadMore: false,
|
||||
loadingMore: false,
|
||||
page: 1
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.fetchSelf();
|
||||
},
|
||||
|
||||
methods: {
|
||||
fetchSelf() {
|
||||
axios.get('/api/v0/groups/self/list')
|
||||
.then(res => {
|
||||
let data = res.data.filter(g => {
|
||||
return g.hasOwnProperty('id') && g.hasOwnProperty('url');
|
||||
})
|
||||
this.groups = data;
|
||||
this.canLoadMore = res.data.length == 4;
|
||||
this.page++;
|
||||
this.isLoaded = true;
|
||||
});
|
||||
},
|
||||
|
||||
loadMore() {
|
||||
this.loadingMore = true;
|
||||
axios.get('/api/v0/groups/self/list', {
|
||||
params: {
|
||||
page: this.page
|
||||
}
|
||||
})
|
||||
.then(res => {
|
||||
let data = res.data.filter(g => {
|
||||
return g.hasOwnProperty('id') && g.hasOwnProperty('url');
|
||||
})
|
||||
this.groups.push(...data);
|
||||
this.canLoadMore = res.data.length == 4;
|
||||
this.page++;
|
||||
this.loadingMore = false;
|
||||
});
|
||||
},
|
||||
|
||||
prettyCount(val) {
|
||||
return App.util.format.count(val);
|
||||
},
|
||||
|
||||
truncate(str) {
|
||||
if(str.length <= 140) {
|
||||
return str;
|
||||
}
|
||||
|
||||
return str.substr(0, 140) + ' ...';
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.my-groups-component {
|
||||
.list-container {
|
||||
margin-bottom: 30vh;
|
||||
}
|
||||
|
||||
.member-label {
|
||||
padding: 2px 5px;
|
||||
font-size: 12px;
|
||||
color: rgba(75, 119, 190, 1);
|
||||
background:rgba(137, 196, 244, 0.2);
|
||||
border:1px solid rgba(137, 196, 244, 0.3);
|
||||
font-weight:400;
|
||||
text-transform: capitalize;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.remote-label {
|
||||
padding: 2px 5px;
|
||||
font-size: 12px;
|
||||
color: #4B5563;
|
||||
background: #F3F4F6;
|
||||
border:1px solid #E5E7EB;
|
||||
font-weight:400;
|
||||
text-transform: capitalize;
|
||||
border-radius: 3px;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,41 @@
|
|||
<template>
|
||||
<div class="col-12 col-md-9" style="height: 100vh - 51px !important;overflow:hidden">
|
||||
<div class="row h-100">
|
||||
<div class="col-12 col-md-8 bg-lighter border-left">
|
||||
<div class="p-4 mb-4">
|
||||
<p class="h4 font-weight-bold mb-1 text-center">Group Invitations</p>
|
||||
</div>
|
||||
<div class="p-4 mb-4">
|
||||
<p class="font-weight-bold text-center text-muted">You don't have any group invites</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-4 bg-white border-left">
|
||||
<div class="p-4">
|
||||
<div class="bg-light rounded-lg border p-3">
|
||||
<p class="lead font-weight-bold mb-0">Send Invite</p>
|
||||
<p class="mb-3">Invite friends to your groups</p>
|
||||
<div class="form-group" style="position: relative;">
|
||||
<span style="position: absolute; top:50%;transform: translateY(-50%);left:15px;padding-right:5px;">
|
||||
<i class="fas fa-search text-lighter"></i>
|
||||
</span>
|
||||
<input class="form-control bg-white rounded-pill" placeholder="Search username..." style="padding-left:40px">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="p-4 mb-2">
|
||||
<p class="h4 font-weight-bold mb-1 text-center">Invitations Sent</p>
|
||||
</div>
|
||||
<div class="px-4 mb-4">
|
||||
<p class="font-weight-bold text-center text-muted">You have not sent any group invites</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script type="text/javascript">
|
||||
export default {
|
||||
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,309 @@
|
|||
<template>
|
||||
<div class="group-notification-component col-12 col-md-9" style="height: 100vh - 51px !important;overflow:hidden">
|
||||
<div class="row h-100 bg-white">
|
||||
<div class="col-12 col-md-8 border-left">
|
||||
<div class="px-5">
|
||||
<div class="my-4">
|
||||
<p class="h1 font-weight-bold mb-1">Group Notifications</p>
|
||||
<!-- <p class="lead text-muted mb-0">Latest notifications from your groups</p> -->
|
||||
</div>
|
||||
<!-- <div class="p-4 mb-4">
|
||||
<p class="font-weight-bold text-center text-muted">You don't have any notifications</p>
|
||||
</div> -->
|
||||
|
||||
<div v-if="notifications.length > 0" v-for="(n, index) in notifications" class="nitem card card-body shadow-none mb-3 py-2 px-0 rounded-pill" style="background-color: #F3F4F6">
|
||||
<div class="media align-items-center px-3">
|
||||
<img class="mr-3 rounded-circle" style="border:1px solid #ccc" :src="n.account.avatar" alt="" width="32px" height="32px">
|
||||
<div class="media-body">
|
||||
<div v-if="n.type == 'group:like'">
|
||||
<p class="my-0">
|
||||
<a :href="getProfileUrl(n.account)" data-placement="bottom" data-toggle="tooltip" :title="n.account.username">{{n.account.local == false ? '@':''}}{{truncate(n.account.username)}}</a> liked your <a v-bind:href="getPostUrl(n.status)">post</a> in <a :href="n.group.url">{{n.group.name}}</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-else-if="n.type == 'group:comment'">
|
||||
<p class="my-0">
|
||||
<a :href="getProfileUrl(n.account)" class="font-weight-bold text-dark word-break" :title="n.account.username">{{n.account.local == false ? '@':''}}{{truncate(n.account.username)}}</a> commented on your <a class="font-weight-bold" v-bind:href="n.status.url">post</a> in <a :href="n.group.url">{{n.group.name}}</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-else-if="n.type == 'mention'">
|
||||
<p class="my-0">
|
||||
<a :href="getProfileUrl(n.account)" data-placement="bottom" data-toggle="tooltip" :title="n.account.username">{{n.account.local == false ? '@':''}}{{truncate(n.account.username)}}</a> <a v-bind:href="mentionUrl(n.status)">mentioned</a> you.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-else-if="n.type == 'group.join.approved'">
|
||||
<p class="my-0">
|
||||
Your application to join <a :href="n.group.url" class="font-weight-bold text-dark word-break" :title="n.group.name">{{truncate(n.group.name)}}</a> was approved!
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-else-if="n.type == 'group.join.rejected'">
|
||||
<p class="my-0">
|
||||
Your application to join <a :href="n.group.url" class="font-weight-bold text-dark word-break" :title="n.group.name">{{truncate(n.group.name)}}</a> was rejected. You can re-apply to join in 6 months.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<p class="my-0">Cannot display notification</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="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>
|
||||
<a v-if="viewContext(n) != '/'" class="btn btn-outline-primary py-0 font-weight-bold" :href="viewContext(n)">View</a>
|
||||
</div> -->
|
||||
|
||||
<div class="align-items-center text-muted">
|
||||
<span class="small" data-toggle="tooltip" data-placement="bottom" :title="n.created_at">{{timeAgo(n.created_at)}}</span>
|
||||
<span>·</span>
|
||||
<div class="dropdown d-inline">
|
||||
<a class="dropdown-toggle text-lighter" href="#" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<i class="far fa-cog fa-sm"></i>
|
||||
</a>
|
||||
|
||||
<div class="dropdown-menu dropdown-menu-right">
|
||||
<a class="dropdown-item font-weight-bold" href="#">Dismiss</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item font-weight-bold" href="#">Help</a>
|
||||
<!-- <a class="dropdown-item font-weight-bold" href="#">Ignore</a> -->
|
||||
<a class="dropdown-item font-weight-bold" href="#">Report</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-4 border-left bg-light">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script type="text/javascript">
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
notifications: [],
|
||||
initialLoad: false,
|
||||
loading: true,
|
||||
page: 1
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.fetchNotifications();
|
||||
},
|
||||
|
||||
methods: {
|
||||
fetchNotifications() {
|
||||
axios.get('/api/pixelfed/v1/accounts/verify_credentials').then(res => {
|
||||
window._sharedData.curUser = res.data;
|
||||
window.App.util.navatar();
|
||||
});
|
||||
axios.get('/api/v0/groups/self/notifications')
|
||||
.then(res => {
|
||||
let data = res.data.filter(n => {
|
||||
if(n.type == 'share' && !n.status) {
|
||||
return false;
|
||||
}
|
||||
if(n.type == 'comment' && !n.status) {
|
||||
return false;
|
||||
}
|
||||
if(n.type == 'mention' && !n.status) {
|
||||
return false;
|
||||
}
|
||||
if(n.type == 'favourite' && !n.status) {
|
||||
return false;
|
||||
}
|
||||
if(n.type == 'follow' && !n.account) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
// let ids = res.data.map(n => n.id);
|
||||
// this.notificationMaxId = Math.max(...ids);
|
||||
this.notifications = data;
|
||||
// $('.notification-card .loader').addClass('d-none');
|
||||
// $('.notification-card .contents').removeClass('d-none');
|
||||
});
|
||||
},
|
||||
|
||||
// infiniteNotifications($state) {
|
||||
// if(this.notificationCursor > 10) {
|
||||
// $state.complete();
|
||||
// return;
|
||||
// }
|
||||
// axios.get('/api/pixelfed/v1/notifications', {
|
||||
// params: {
|
||||
// max_id: this.notificationMaxId
|
||||
// }
|
||||
// }).then(res => {
|
||||
// if(res.data.length) {
|
||||
// let data = res.data.filter(n => {
|
||||
// if(n.type == 'share' && !n.status) {
|
||||
// return false;
|
||||
// }
|
||||
// if(n.type == 'comment' && !n.status) {
|
||||
// return false;
|
||||
// }
|
||||
// if(n.type == 'mention' && !n.status) {
|
||||
// return false;
|
||||
// }
|
||||
// if(n.type == 'favourite' && !n.status) {
|
||||
// return false;
|
||||
// }
|
||||
// if(n.type == 'follow' && !n.account) {
|
||||
// return false;
|
||||
// }
|
||||
// if(_.find(this.notifications, {id: n.id})) {
|
||||
// return false;
|
||||
// }
|
||||
// return true;
|
||||
// });
|
||||
// this.notifications.push(...data);
|
||||
// this.notificationCursor++;
|
||||
// $state.loaded();
|
||||
// } else {
|
||||
// $state.complete();
|
||||
// }
|
||||
// });
|
||||
// },
|
||||
|
||||
truncate(text) {
|
||||
if(text.length <= 15) {
|
||||
return text;
|
||||
}
|
||||
|
||||
return text.slice(0,15) + '...'
|
||||
},
|
||||
|
||||
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;
|
||||
},
|
||||
|
||||
followProfile(n) {
|
||||
let self = this;
|
||||
let id = n.account.id;
|
||||
axios.post('/i/follow', {
|
||||
item: id
|
||||
}).then(res => {
|
||||
self.notifications.map(notification => {
|
||||
if(notification.account.id === id) {
|
||||
notification.relationship.following = true;
|
||||
}
|
||||
});
|
||||
}).catch(err => {
|
||||
if(err.response.data.message) {
|
||||
swal('Error', err.response.data.message, 'error');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
viewContext(n) {
|
||||
switch(n.type) {
|
||||
case 'follow':
|
||||
return n.account.url;
|
||||
break;
|
||||
case 'mention':
|
||||
return n.status.url;
|
||||
break;
|
||||
case 'like':
|
||||
case 'favourite':
|
||||
case 'comment':
|
||||
return n.status.url;
|
||||
break;
|
||||
case 'tagged':
|
||||
return n.tagged.post_url;
|
||||
break;
|
||||
case 'direct':
|
||||
return '/account/direct/t/'+n.account.id;
|
||||
break
|
||||
}
|
||||
return '/';
|
||||
},
|
||||
|
||||
getProfileUrl(account) {
|
||||
if(account.local == true) {
|
||||
return account.url;
|
||||
}
|
||||
|
||||
return '/i/web/profile/_/' + account.id;
|
||||
},
|
||||
|
||||
getPostUrl(status) {
|
||||
if(status.local == true) {
|
||||
return status.url;
|
||||
}
|
||||
|
||||
return '/i/web/post/_/' + status.account.id + '/' + status.id;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.group-notification-component {
|
||||
.dropdown-toggle::after {
|
||||
content: '';
|
||||
display: none;
|
||||
}
|
||||
|
||||
.nitem {
|
||||
a {
|
||||
color: #000;
|
||||
font-weight: 700 !important;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
color: #121416 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,47 @@
|
|||
<template>
|
||||
<div class="col-12 col-md-9" style="height: 100vh - 51px !important;overflow:hidden">
|
||||
<div class="row h-100 bg-lighter">
|
||||
<div class="col-12 col-md-8 border-left">
|
||||
<div class="d-flex justify-content-center">
|
||||
<div class="p-4 mb-4">
|
||||
<p class="h4 font-weight-bold mb-1">Find a Remote Group</p>
|
||||
<p class="lead text-muted">Search and explore remote federated groups.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-5">
|
||||
<div class="p-4 mb-4">
|
||||
<div class="form-group">
|
||||
<label>Group URL</label>
|
||||
<input type="text" class="form-control form-control-lg rounded-pill bg-white border" placeholder="https://pixelfed.social/groups/328323406233735168" v-model="q">
|
||||
</div>
|
||||
<button class="btn btn-primary btn-block btn-lg rounded-pill font-weight-bold">Search</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-4 bg-white border-left">
|
||||
<div class="my-4">
|
||||
<h4 class="font-weight-bold">Tips</h4>
|
||||
<ul class="pl-3">
|
||||
<li class="font-weight-bold">Some remote groups are not supported*</li>
|
||||
<li>Read and comply with group rules defined by group admins</li>
|
||||
<li>Use the full <span class="font-weight-bold">Group URL</span> including <code>https://</code></li>
|
||||
<li>Joining private groups requires manual approval from group admins, you will recieve a notification when your membership is approved</li>
|
||||
<li>Inviting people to remote groups is not supported yet</li>
|
||||
<li>Your group membership may be terminated at any time by group admins</li>
|
||||
</ul>
|
||||
<p class="small">* Some remote groups may not be compatible, we are working to support other group implementations</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script type="text/javascript">
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
q: undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
11
resources/assets/components/groups/partials/ShareMenu.vue
Normal file
11
resources/assets/components/groups/partials/ShareMenu.vue
Normal file
|
@ -0,0 +1,11 @@
|
|||
<template>
|
||||
<div class="share-menu-component">
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script type="text/javascript">
|
||||
export default {
|
||||
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,304 @@
|
|||
<template>
|
||||
<div class="group-post-header media">
|
||||
<div v-if="showGroupHeader" style="position: relative;" class="mb-1">
|
||||
<img
|
||||
v-if="group.hasOwnProperty('metadata') && (group.metadata.hasOwnProperty('avatar') || group.metadata.hasOwnProperty('header'))"
|
||||
class="rounded-lg box-shadow mr-2"
|
||||
:src="group.metadata.hasOwnProperty('header') ? group.metadata.header.url : group.metadata.avatar.url"
|
||||
width="52"
|
||||
height="52"
|
||||
onerror="this.onerror=null;this.src='/storage/avatars/default.png?v=2'"
|
||||
alt="avatar"
|
||||
style="object-fit:cover;"
|
||||
/>
|
||||
<span
|
||||
v-else
|
||||
class="d-block rounded-lg box-shadow mr-2 bg-primary"
|
||||
style="width: 52px;height:52px;"
|
||||
></span>
|
||||
<img
|
||||
class="rounded-circle box-shadow border mr-2"
|
||||
:src="status.account.avatar"
|
||||
width="36"
|
||||
height="36"
|
||||
onerror="this.onerror=null;this.src='/storage/avatars/default.png?v=2'"
|
||||
alt="avatar"
|
||||
style="position: absolute; bottom:-4px; right:-4px;"
|
||||
/>
|
||||
</div>
|
||||
<img
|
||||
v-else
|
||||
class="rounded-circle box-shadow mr-2"
|
||||
:src="status.account.avatar"
|
||||
width="42"
|
||||
height="42"
|
||||
onerror="this.onerror=null;this.src='/storage/avatars/default.png?v=2'"
|
||||
alt="avatar"
|
||||
/>
|
||||
<div class="media-body">
|
||||
<div class="pl-2 d-flex align-items-top">
|
||||
<div>
|
||||
<p class="mb-0">
|
||||
<router-link
|
||||
v-if="showGroupHeader && group"
|
||||
:to="`/groups/${status.gid}`"
|
||||
class="group-name-link username"
|
||||
>
|
||||
{{ group.name }}
|
||||
</router-link>
|
||||
|
||||
<router-link
|
||||
v-else
|
||||
:to="`/groups/${status.gid}/user/${status?.account.id}`"
|
||||
class="group-name-link username"
|
||||
v-html="statusCardUsernameFormat(status)"
|
||||
>
|
||||
Loading...
|
||||
</router-link>
|
||||
|
||||
<span v-if="showGroupChevron">
|
||||
<span class="text-muted" style="padding-left:2px;padding-right:2px;">
|
||||
<i class="fas fa-caret-right"></i>
|
||||
</span>
|
||||
<span>
|
||||
<router-link
|
||||
:to="`/groups/${status.gid}`"
|
||||
class="group-name-link"
|
||||
>
|
||||
{{ group.name }}
|
||||
</router-link>
|
||||
</span>
|
||||
</span>
|
||||
</p>
|
||||
<p class="mb-0 mt-n1">
|
||||
<span
|
||||
v-if="showGroupHeader && group"
|
||||
style="font-size:13px"
|
||||
>
|
||||
<router-link
|
||||
:to="`/groups/${status.gid}/user/${status?.account.id}`"
|
||||
class="group-name-link-small username"
|
||||
v-html="statusCardUsernameFormat(status)"
|
||||
>
|
||||
Loading...
|
||||
</router-link>
|
||||
<span class="text-lighter" style="padding-left:2px;padding-right:2px;">·</span>
|
||||
<router-link
|
||||
:to="`/groups/${status.gid}/p/${status.id}`"
|
||||
class="font-weight-light text-muted"
|
||||
>
|
||||
{{shortTimestamp(status.created_at)}}
|
||||
</router-link>
|
||||
<span class="text-lighter" style="padding-left:2px;padding-right:2px;">·</span>
|
||||
<span class="text-muted"><i class="fas fa-globe"></i></span>
|
||||
</span>
|
||||
<span v-else>
|
||||
<router-link
|
||||
:to="`/groups/${status.gid}/p/${status.id}`"
|
||||
class="font-weight-light text-muted small"
|
||||
>
|
||||
{{shortTimestamp(status.created_at)}}
|
||||
</router-link>
|
||||
<span class="text-lighter" style="padding-left:2px;padding-right:2px;">·</span>
|
||||
<span class="text-muted small"><i class="fas fa-globe"></i></span>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<div v-if="profile" class="text-right" style="flex-grow:1;">
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-link dropdown-toggle" type="button" data-toggle="dropdown" aria-expanded="false">
|
||||
<span class="fas fa-ellipsis-h text-lighter"></span>
|
||||
</button>
|
||||
<div class="dropdown-menu dropdown-menu-right">
|
||||
<a class="dropdown-item" href="#">View Post</a>
|
||||
<a class="dropdown-item" href="#">View Profile</a>
|
||||
<a class="dropdown-item" href="#">Copy Link</a>
|
||||
<a class="dropdown-item" href="#" @click.prevent="sendReport()">Report</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item text-danger" href="#">Delete</a>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <button class="btn btn-link text-dark py-0" type="button" @click="ctxMenu()">
|
||||
<span class="fas fa-ellipsis-h text-lighter"></span>
|
||||
<span class="sr-only">Post Menu</span>
|
||||
</button> -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
group: {
|
||||
type: Object
|
||||
},
|
||||
status: {
|
||||
type: Object
|
||||
},
|
||||
profile: {
|
||||
type: Object
|
||||
},
|
||||
showGroupHeader: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
showGroupChevron: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
reportTypes: [
|
||||
{ key: "spam", title: "It's spam" },
|
||||
{ key: "sensitive", title: "Nudity or sexual activity" },
|
||||
{ key: "abusive", title: "Bullying or harassment" },
|
||||
{ key: "underage", title: "I think this account is underage" },
|
||||
{ key: "violence", title: "Violence or dangerous organizations" },
|
||||
{ key: "copyright", title: "Copyright infringement" },
|
||||
{ key: "impersonation", title: "Impersonation" },
|
||||
{ key: "scam", title: "Scam or fraud" },
|
||||
{ key: "terrorism", title: "Terrorism or terrorism-related content" }
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
formatCount(count) {
|
||||
return App.util.format.count(count);
|
||||
},
|
||||
|
||||
statusUrl(status) {
|
||||
return '/groups/' + status.gid + '/p/' + status.id;
|
||||
},
|
||||
|
||||
profileUrl(status) {
|
||||
return '/groups/' + status.gid + '/user/' + status.account.id;
|
||||
},
|
||||
|
||||
timestampFormat(timestamp) {
|
||||
let ts = new Date(timestamp);
|
||||
return ts.toDateString() + ' ' + ts.toLocaleTimeString();
|
||||
},
|
||||
|
||||
shortTimestamp(ts) {
|
||||
return window.App.util.format.timeAgo(ts);
|
||||
},
|
||||
|
||||
statusCardUsernameFormat(status) {
|
||||
if(status.account.local == true) {
|
||||
return status.account.username;
|
||||
}
|
||||
|
||||
let fmt = window.App.config.username.remote.format;
|
||||
let txt = window.App.config.username.remote.custom;
|
||||
let usr = status.account.username;
|
||||
let dom = document.createElement('a');
|
||||
dom.href = status.account.url;
|
||||
dom = dom.hostname;
|
||||
|
||||
switch(fmt) {
|
||||
case '@':
|
||||
return usr + '<span class="text-lighter font-weight-bold">@' + dom + '</span>';
|
||||
break;
|
||||
|
||||
case 'from':
|
||||
return usr + '<span class="text-lighter font-weight-bold"> <span class="font-weight-normal">from</span> ' + dom + '</span>';
|
||||
break;
|
||||
|
||||
case 'custom':
|
||||
return usr + '<span class="text-lighter font-weight-bold"> ' + txt + ' ' + dom + '</span>';
|
||||
break;
|
||||
|
||||
default:
|
||||
return usr + '<span class="text-lighter font-weight-bold">@' + dom + '</span>';
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
sendReport(type) {
|
||||
let el = document.createElement('div');
|
||||
el.classList.add('list-group');
|
||||
this.reportTypes.forEach(rt => {
|
||||
let button = document.createElement('button');
|
||||
button.classList.add('list-group-item', 'small');
|
||||
button.innerHTML = rt.title;
|
||||
button.onclick = () => {
|
||||
document.dispatchEvent(new CustomEvent('reportOption', {
|
||||
detail: { key: rt.key, title: rt.title }
|
||||
}));
|
||||
};
|
||||
el.appendChild(button);
|
||||
});
|
||||
|
||||
let wrapper = document.createElement('div');
|
||||
wrapper.appendChild(el);
|
||||
|
||||
swal({
|
||||
title: "Report Content",
|
||||
icon: "warning",
|
||||
content: wrapper,
|
||||
buttons: false
|
||||
});
|
||||
|
||||
document.addEventListener('reportOption', (event) => {
|
||||
console.log(event.detail);
|
||||
this.showConfirmation(event.detail);
|
||||
}, { once: true });
|
||||
},
|
||||
showConfirmation(option) {
|
||||
console.log(option)
|
||||
swal({
|
||||
title: "Confirmation",
|
||||
text: `You selected ${option.title}. Do you want to proceed?`,
|
||||
icon: "info",
|
||||
buttons: true
|
||||
}).then((confirm) => {
|
||||
if (confirm) {
|
||||
axios.post(`/api/v0/groups/${this.status.gid}/report/create`, {
|
||||
'type': option.key,
|
||||
'id': this.status.id,
|
||||
}).then(res => {
|
||||
swal("Confirmed!", "Your report has been submitted.", "success");
|
||||
})
|
||||
} else {
|
||||
swal("Cancelled", "Your report was not submitted.", "error");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.group-post-header {
|
||||
.btn::focus {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.dropdown-toggle::after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.group-name-link {
|
||||
color: var(--body-color) !important;
|
||||
word-break: break-word !important;
|
||||
word-wrap: break-word !important;
|
||||
text-decoration: none;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.group-name-link-small {
|
||||
color: var(--body-color) !important;
|
||||
word-break: break-word !important;
|
||||
word-wrap: break-word !important;
|
||||
text-decoration: none;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,58 @@
|
|||
<template>
|
||||
<div class="card shadow-sm" style="border-radius: 18px !important;">
|
||||
<div class="card-body pb-0">
|
||||
<div class="card card-body border shadow-none mb-3" style="background-color: #E5E7EB;">
|
||||
<div class="media p-md-4">
|
||||
<div class="mr-4 pt-2">
|
||||
<i class="fas fa-lock fa-2x"></i>
|
||||
</div>
|
||||
<div class="media-body" style="max-width: 320px">
|
||||
<p class="lead font-weight-bold mb-1">This content isn't available right now</p>
|
||||
<p class="mb-0" style="font-size: 12px;letter-spacing:-0.3px;">When this happens, it's usually because the owner only shared it with a small group of people, changed who can see it, or it's been deleted.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<comment-drawer
|
||||
v-if="showCommentDrawer"
|
||||
:permalink-mode="permalinkMode"
|
||||
:permalink-status="childContext"
|
||||
:status="status"
|
||||
:profile="profile"
|
||||
:group-id="groupId"
|
||||
:can-reply="false" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CommentDrawer from '@/groups/partials/CommentDrawer.vue';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
showCommentDrawer: {
|
||||
type: Boolean,
|
||||
},
|
||||
permalinkMode: {
|
||||
type: Boolean,
|
||||
},
|
||||
childContext: {
|
||||
type: Object
|
||||
},
|
||||
status: {
|
||||
type: Object
|
||||
},
|
||||
profile: {
|
||||
type: Object
|
||||
},
|
||||
groupId: {
|
||||
type: String
|
||||
},
|
||||
},
|
||||
|
||||
components: {
|
||||
"comment-drawer": CommentDrawer,
|
||||
}
|
||||
}
|
||||
</script>
|
23
resources/assets/components/groups/sections/Loader.vue
Normal file
23
resources/assets/components/groups/sections/Loader.vue
Normal file
|
@ -0,0 +1,23 @@
|
|||
<template>
|
||||
<div class="w-100 h-100">
|
||||
<div v-if="!loaded" class="d-flex w-100 h-100 py-5 justify-content-center align-items-center">
|
||||
<div class="text-center">
|
||||
<div class="spinner-border" role="status">
|
||||
<span class="sr-only">Loading...</span>
|
||||
</div>
|
||||
<p class="text-center font-weight-bold mt-1">Loading...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
loaded: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
316
resources/assets/components/groups/sections/Sidebar.vue
Normal file
316
resources/assets/components/groups/sections/Sidebar.vue
Normal file
|
@ -0,0 +1,316 @@
|
|||
<template>
|
||||
<div class="col-3 shadow groups-sidenav">
|
||||
<div class="p-1">
|
||||
<div class="d-flex justify-content-between align-items-center py-3">
|
||||
<p class="h2 font-weight-bold mb-0">Groups</p>
|
||||
<a class="btn btn-light px-2 rounded-circle" href="/settings/home">
|
||||
<i class="fas fa-cog fa-lg"></i>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<autocomplete
|
||||
:search="autocompleteSearch"
|
||||
placeholder="Search groups by name"
|
||||
aria-label="Search groups by name"
|
||||
:get-result-value="getSearchResultValue"
|
||||
:debounceTime="700"
|
||||
@submit="onSearchSubmit"
|
||||
ref="autocomplete"
|
||||
>
|
||||
<template #result="{ result, props }">
|
||||
<li
|
||||
v-bind="props"
|
||||
class="autocomplete-result"
|
||||
>
|
||||
|
||||
<div class="media align-items-center">
|
||||
<img v-if="result.local && result.metadata && result.metadata.hasOwnProperty('header') && result.metadata.header.hasOwnProperty('url')" :src="result.metadata.header.url" width="32" height="32">
|
||||
<div v-else class="icon-placeholder">
|
||||
<i class="fal fa-user-friends"></i>
|
||||
</div>
|
||||
<div class="media-body text-truncate mr-3">
|
||||
<p class="result-name mb-n1 font-weight-bold">
|
||||
{{ truncateName(result.name) }}
|
||||
<span v-if="result.verified" class="fa-stack ml-n2" title="Verified Group" data-toggle="tooltip">
|
||||
<i class="fas fa-circle fa-stack-1x fa-lg" style="color: #22a7f0cc;font-size:18px"></i>
|
||||
<i class="fas fa-check fa-stack-1x text-white" style="font-size:10px"></i>
|
||||
</span>
|
||||
</p>
|
||||
<p class="mb-0 text-muted" style="font-size: 10px;">
|
||||
<span v-if="!result.local" title="Remote Group">
|
||||
<i class="far fa-globe"></i>
|
||||
</span>
|
||||
<span v-if="!result.local">·</span>
|
||||
<span class="font-weight-bold">{{ result.member_count }} members</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</template>
|
||||
</autocomplete>
|
||||
</div>
|
||||
|
||||
<!-- <button
|
||||
class="btn btn-light group-nav-btn"
|
||||
:class="{ active: tab == 'feed' }"
|
||||
@click="switchTab('feed')">
|
||||
<div class="group-nav-btn-icon">
|
||||
<i class="fas fa-list"></i>
|
||||
</div>
|
||||
<div class="group-nav-btn-name">
|
||||
Your Feed
|
||||
</div>
|
||||
</button> -->
|
||||
|
||||
<template v-for="tab in tabs">
|
||||
<router-link
|
||||
class="btn btn-light group-nav-btn"
|
||||
:to="tab.path">
|
||||
<div class="group-nav-btn-icon">
|
||||
<i :class="tab.icon"></i>
|
||||
</div>
|
||||
<div class="group-nav-btn-name">
|
||||
{{ tab.name }}
|
||||
</div>
|
||||
</router-link>
|
||||
</template>
|
||||
|
||||
<router-link
|
||||
to="/groups/create"
|
||||
class="btn btn-primary btn-block rounded-pill font-weight-bold mt-3">
|
||||
<i class="fas fa-plus mr-2"></i> Create New Group
|
||||
</router-link>
|
||||
|
||||
<hr>
|
||||
<!-- <div v-for="group in groups" class="ml-2">
|
||||
<div class="card shadow-sm border text-decoration-none text-dark">
|
||||
<img v-if="group.metadata && group.metadata.hasOwnProperty('header')" :src="group.metadata.header.url" class="card-img-top" style="width: 100%; height: auto;object-fit: cover;max-height: 160px;">
|
||||
<div v-else class="bg-primary" style="width:100%;height:160px;"></div>
|
||||
<div class="card-body">
|
||||
<div class="lead font-weight-bold d-flex align-items-top" style="height: 60px;">
|
||||
{{ group.name }}
|
||||
<span v-if="group.verified" class="fa-stack ml-n2 mt-n2">
|
||||
<i class="fas fa-circle fa-stack-1x fa-lg" style="color: #22a7f0cc;font-size:18px"></i>
|
||||
<i class="fas fa-check fa-stack-1x text-white" style="font-size:10px"></i>
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-muted font-weight-light d-flex justify-content-between">
|
||||
<span>{{group.member_count}} Members</span>
|
||||
<span style="font-size: 12px;padding: 2px 5px;color: rgba(75, 119, 190, 1);background:rgba(137, 196, 244, 0.2);border:1px solid rgba(137, 196, 244, 0.3);font-weight:400;text-transform: capitalize;" class="rounded">{{ group.self.role }}</span>
|
||||
</div>
|
||||
<hr>
|
||||
<p class="mb-0">
|
||||
<a class="btn btn-light btn-block border rounded-lg font-weight-bold" :href="group.url">View Group</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Autocomplete from '@trevoreyre/autocomplete-vue'
|
||||
import '@trevoreyre/autocomplete-vue/dist/style.css'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
initialLoad: false,
|
||||
tabs: [
|
||||
{ name: 'Your Feed', icon: 'fas fa-list', path: '/groups/feed' },
|
||||
{ name: 'Discover', icon: 'fas fa-compass', path: '/groups/discover' },
|
||||
{ name: 'Your Groups', icon: 'fas fa-list', path: '/groups/joins' },
|
||||
// { name: 'Notifications', icon: 'far fa-bell', path: '/groups/notifications' },
|
||||
// { name: 'Find Remote Group', icon: 'far fa-search-plus', path: '/groups/search' },
|
||||
],
|
||||
config: {},
|
||||
groups: [],
|
||||
profile: {},
|
||||
tab: null,
|
||||
searchQuery: undefined,
|
||||
};
|
||||
},
|
||||
|
||||
components: {
|
||||
'autocomplete': Autocomplete,
|
||||
},
|
||||
|
||||
methods: {
|
||||
autocompleteSearch(input) {
|
||||
if (!input || input.length < 2) {
|
||||
return [];
|
||||
};
|
||||
|
||||
this.searchQuery = input;
|
||||
// this.tab = 'searchresults';
|
||||
|
||||
if(input.startsWith('http')) {
|
||||
let url = new URL(input);
|
||||
if(url.hostname == location.hostname) {
|
||||
location.href = input;
|
||||
return [];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
if(input.startsWith('#')) {
|
||||
this.$bvToast.toast(input, {
|
||||
title: 'Hashtag detected',
|
||||
variant: 'info',
|
||||
autoHideDelay: 5000
|
||||
});
|
||||
return [];
|
||||
}
|
||||
|
||||
return axios.post('/api/v0/groups/search/global', {
|
||||
q: input,
|
||||
v: '0.2'
|
||||
})
|
||||
.then(res => {
|
||||
this.searchLoading = false;
|
||||
return res.data;
|
||||
}).catch(err => {
|
||||
|
||||
if(err.response.status === 422) {
|
||||
this.$bvToast.toast(err.response.data.error.message, {
|
||||
title: 'Cannot display search results',
|
||||
variant: 'danger',
|
||||
autoHideDelay: 5000
|
||||
});
|
||||
}
|
||||
|
||||
return [];
|
||||
})
|
||||
},
|
||||
|
||||
getSearchResultValue(result) {
|
||||
return result.name;
|
||||
},
|
||||
|
||||
onSearchSubmit(result) {
|
||||
if (result.length < 1) {
|
||||
return [];
|
||||
}
|
||||
|
||||
location.href = result.url;
|
||||
},
|
||||
|
||||
truncateName(val) {
|
||||
if(val.length < 24) {
|
||||
return val;
|
||||
}
|
||||
|
||||
return val.substr(0, 23) + '...';
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.groups-sidenav {
|
||||
display: none;
|
||||
font-family: var(--font-family-sans-serif);
|
||||
|
||||
@media(min-width: 768px) {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
background: #fff;
|
||||
top: 74px;
|
||||
border: none;
|
||||
overflow: hidden;
|
||||
z-index: 1;
|
||||
position: sticky;
|
||||
}
|
||||
|
||||
.group-nav-btn {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding-left: 0;
|
||||
padding-top: 0.3rem;
|
||||
padding-bottom: 0.3rem;
|
||||
margin-bottom: 0.3rem;
|
||||
border-radius: 1.5rem;
|
||||
text-align: left;
|
||||
color: #6c757d;
|
||||
background-color: transparent;
|
||||
border-color: transparent;
|
||||
justify-content: flex-start;
|
||||
|
||||
&.active {
|
||||
background-color: #EFF6FF !important;
|
||||
border:1px solid #DBEAFE !important;
|
||||
color: #212529;
|
||||
|
||||
.group-nav-btn-icon {
|
||||
background-color: #2c78bf !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
}
|
||||
|
||||
&-icon {
|
||||
display: inline-flex;
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
padding: 12px;
|
||||
background-color: #E5E7EB;
|
||||
border-radius: 17px;
|
||||
margin: auto 0.3rem;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
&-name {
|
||||
display: inline-block;
|
||||
margin-left: 0.3rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
.autocomplete-input {
|
||||
height: 2.375rem;
|
||||
background-color: #f8f9fa !important;
|
||||
font-size: 0.9rem;
|
||||
color: #495057;
|
||||
border-radius: 50rem;
|
||||
border-color: transparent;
|
||||
|
||||
&:focus,
|
||||
&[aria-expanded=true] {
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
.autocomplete-result {
|
||||
background: none;
|
||||
padding: 12px;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background-color: #EFF6FF !important;
|
||||
}
|
||||
|
||||
.media {
|
||||
img {
|
||||
object-fit: cover;
|
||||
border-radius: 4px;
|
||||
margin-right: 0.6rem;
|
||||
}
|
||||
|
||||
.icon-placeholder {
|
||||
display: flex;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background-color: #2c78bf;
|
||||
border-radius: 4px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: #fff;
|
||||
margin-right: 0.6rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
4
resources/assets/js/group-status.js
vendored
Normal file
4
resources/assets/js/group-status.js
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
Vue.component(
|
||||
'gs-permalink',
|
||||
require('./components/GroupStatusPermalink.vue').default
|
||||
);
|
4
resources/assets/js/group-topic-feed.js
vendored
Normal file
4
resources/assets/js/group-topic-feed.js
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
Vue.component(
|
||||
'group-topic-feed',
|
||||
require('./../components/groups/GroupTopicFeed.vue').default
|
||||
);
|
29
resources/assets/js/groups.js
vendored
Normal file
29
resources/assets/js/groups.js
vendored
Normal file
|
@ -0,0 +1,29 @@
|
|||
Vue.component(
|
||||
'group-component',
|
||||
require('./../components/Groups.vue').default
|
||||
);
|
||||
|
||||
Vue.component(
|
||||
'groups-home',
|
||||
require('./../components/groups/GroupsHome.vue').default
|
||||
);
|
||||
|
||||
Vue.component(
|
||||
'group-feed',
|
||||
require('./../components/groups/GroupFeed.vue').default
|
||||
);
|
||||
|
||||
Vue.component(
|
||||
'group-settings',
|
||||
require('./../components/groups/GroupSettings.vue').default
|
||||
);
|
||||
|
||||
Vue.component(
|
||||
'group-profile',
|
||||
require('./../components/groups/GroupProfile.vue').default
|
||||
);
|
||||
|
||||
Vue.component(
|
||||
'groups-invite',
|
||||
require('./../components/groups/GroupInvite.vue').default
|
||||
);
|
3
webpack.mix.js
vendored
3
webpack.mix.js
vendored
|
@ -40,6 +40,9 @@ mix.js('resources/assets/js/app.js', 'public/js')
|
|||
.js('resources/assets/js/admin_invite.js', 'public/js')
|
||||
.js('resources/assets/js/landing.js', 'public/js')
|
||||
.js('resources/assets/js/remote_auth.js', 'public/js')
|
||||
.js('resources/assets/js/groups.js', 'public/js')
|
||||
.js('resources/assets/js/group-status.js', 'public/js')
|
||||
.js('resources/assets/js/group-topic-feed.js', 'public/js')
|
||||
.vue({ version: 2 });
|
||||
|
||||
mix.extract();
|
||||
|
|
Loading…
Reference in a new issue