kopia lustrzana https://github.com/nextcloud/social
Merge pull request #793 from StCyr/bugfix/756/singlepost-improvement
WIP: single post timeline improvementpull/1073/head
commit
e156bcdada
|
@ -50,7 +50,7 @@ return [
|
|||
['name' => 'Navigation#resizedGetPublic', 'url' => '/document/public/resized', 'verb' => 'GET'],
|
||||
|
||||
['name' => 'ActivityPub#actor', 'url' => '/users/{username}', 'verb' => 'GET'],
|
||||
['name' => 'ActivityPub#actorAlias', 'url' => '/@{username}/', 'verb' => 'GET'],
|
||||
['name' => 'ActivityPub#actorAlias', 'url' => '/@{username}', 'verb' => 'GET'],
|
||||
['name' => 'ActivityPub#inbox', 'url' => '/@{username}/inbox', 'verb' => 'POST'],
|
||||
['name' => 'ActivityPub#getInbox', 'url' => '/@{username}/inbox', 'verb' => 'GET'],
|
||||
['name' => 'ActivityPub#sharedInbox', 'url' => '/inbox', 'verb' => 'POST'],
|
||||
|
|
|
@ -76,8 +76,15 @@ class SocialLimitsQueryBuilder extends SocialCrossQueryBuilder {
|
|||
* Limit the request to the Id (string)
|
||||
*
|
||||
* @param string $id
|
||||
* @param bool $prim
|
||||
*/
|
||||
public function limitToInReplyTo(string $id) {
|
||||
public function limitToInReplyTo(string $id, bool $prim = false) {
|
||||
if ($prim) {
|
||||
$this->limitToDBField('in_reply_to_prim', $this->prim($id), false);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->limitToDBField('in_reply_to', $id, false);
|
||||
}
|
||||
|
||||
|
|
|
@ -274,11 +274,7 @@ class StreamRequest extends StreamRequestBuilder {
|
|||
$qb->limitToInReplyTo($id);
|
||||
$qb->limitPaginate($since, $limit);
|
||||
|
||||
$expr = $qb->expr();
|
||||
$qb->linkToCacheActors('ca', 's.attributed_to_prim');
|
||||
|
||||
$qb->andWhere($expr->eq('s.attributed_to', 'ca.id_prim'));
|
||||
|
||||
if ($asViewer) {
|
||||
$qb->limitToViewer('sd', 'f', true);
|
||||
$qb->leftJoinStreamAction();
|
||||
|
|
|
@ -39,7 +39,7 @@
|
|||
<span>In reply to</span>
|
||||
<actor-avatar :actor="replyTo.actor_info" :size="16" />
|
||||
<strong>{{ replyTo.actor_info.account }}</strong>
|
||||
<a class="icon-close" @click="replyTo=null" />
|
||||
<a class="icon-close" @click="closeReply()" />
|
||||
</p>
|
||||
<div class="reply-to-preview">
|
||||
{{ replyTo.content }}
|
||||
|
@ -610,6 +610,7 @@ export default {
|
|||
mounted() {
|
||||
this.$root.$on('composer-reply', (data) => {
|
||||
this.replyTo = data
|
||||
this.type = 'direct'
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
|
@ -737,7 +738,7 @@ export default {
|
|||
|
||||
let contentHtml = element.innerHTML
|
||||
|
||||
// Extract mentions from content and create an array ot of them
|
||||
// Extract mentions from content and create an array out of them
|
||||
let to = []
|
||||
const mentionRegex = /<span class="mention"[^>]+><a[^>]+><img[^>]+>@([\w-_.]+@[\w-.]+)/g
|
||||
let match = null
|
||||
|
@ -748,6 +749,11 @@ export default {
|
|||
}
|
||||
} while (match)
|
||||
|
||||
// Add author of original post in case of reply
|
||||
if (this.replyTo !== null) {
|
||||
to.push(this.replyTo.actor_info.account)
|
||||
}
|
||||
|
||||
// Extract hashtags from content and create an array ot of them
|
||||
const hashtagRegex = />#([^<]+)</g
|
||||
let hashtags = []
|
||||
|
@ -824,6 +830,11 @@ export default {
|
|||
})
|
||||
|
||||
},
|
||||
closeReply() {
|
||||
this.replyTo = null
|
||||
// View may want to hide the composer
|
||||
this.$store.commit('setComposerDisplayStatus', false)
|
||||
},
|
||||
remoteSearchAccounts(text) {
|
||||
return axios.get(generateUrl('apps/social/api/v1/global/accounts/search?search=' + text))
|
||||
},
|
||||
|
|
|
@ -22,7 +22,10 @@
|
|||
|
||||
<template>
|
||||
<div class="emptycontent">
|
||||
<img :src="imageUrl" class="icon-illustration" alt="">
|
||||
<img v-if="item.image"
|
||||
:src="imageUrl"
|
||||
class="icon-illustration"
|
||||
alt="">
|
||||
<h2>{{ item.title }}</h2>
|
||||
<p>{{ item.description }}</p>
|
||||
</div>
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
|
||||
<template>
|
||||
<!-- Show button only if user is authenticated and she is not the same as the account viewed -->
|
||||
<div v-if="!serverData.public && actorInfo && actorInfo.viewerLink!='viewer'">
|
||||
<div v-if="!serverData.public && accountInfo && accountInfo.viewerLink!='viewer'">
|
||||
<button v-if="isCurrentUserFollowing" :class="{'icon-loading-small': followLoading}"
|
||||
@click="unfollow()"
|
||||
@mouseover="followingText=t('social', 'Unfollow')" @mouseleave="followingText=t('social', 'Following')">
|
||||
|
@ -36,17 +36,23 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import accountMixins from '../mixins/accountMixins'
|
||||
import currentUser from '../mixins/currentUserMixin'
|
||||
|
||||
export default {
|
||||
name: 'FollowButton',
|
||||
mixins: [
|
||||
accountMixins,
|
||||
currentUser
|
||||
],
|
||||
props: {
|
||||
account: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
uid: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
data: function() {
|
||||
|
@ -55,9 +61,6 @@ export default {
|
|||
}
|
||||
},
|
||||
computed: {
|
||||
actorInfo() {
|
||||
return this.$store.getters.getAccount(this.account)
|
||||
},
|
||||
followLoading() {
|
||||
return false
|
||||
},
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<masonry>
|
||||
<div v-for="(item, index) in attachments" :key="index">
|
||||
<img :src="generateUrl('/apps/social/document/get/resized?id=' + item.id)" @click="showModal(index)">
|
||||
<img :src="imageUrl(item)" @click="showModal(index)">
|
||||
</div>
|
||||
<modal v-show="modal" :has-previous="current > 0" :has-next="current < (attachments.length - 1)"
|
||||
size="full" @close="closeModal" @previous="showPrevious"
|
||||
|
@ -15,6 +15,7 @@
|
|||
|
||||
<script>
|
||||
|
||||
import serverData from '../mixins/serverData'
|
||||
import Modal from '@nextcloud/vue/dist/Components/Modal'
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
|
||||
|
@ -23,7 +24,9 @@ export default {
|
|||
components: {
|
||||
Modal
|
||||
},
|
||||
mixins: [],
|
||||
mixins: [
|
||||
serverData
|
||||
],
|
||||
props: {
|
||||
attachments: {
|
||||
type: Array,
|
||||
|
@ -37,7 +40,24 @@ export default {
|
|||
}
|
||||
},
|
||||
methods: {
|
||||
displayResizedImage() {
|
||||
/**
|
||||
* @function imageUrl
|
||||
* @description Returns the URL where to get a resized version of the attachement
|
||||
* @param {object} item - The attachment
|
||||
* @returns {string} The URL
|
||||
*/
|
||||
imageUrl(item) {
|
||||
if (this.serverData.public) {
|
||||
return generateUrl('/apps/social/document/public/resized?id=' + item.id)
|
||||
} else {
|
||||
return generateUrl('/apps/social/document/get/resized?id=' + item.id)
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @function displayImage
|
||||
* @description Displays the currently selected attachment's image
|
||||
*/
|
||||
displayImage() {
|
||||
var canvas = this.$refs.modalCanvas
|
||||
var ctx = canvas.getContext('2d')
|
||||
var img = new Image()
|
||||
|
@ -60,7 +80,7 @@ export default {
|
|||
},
|
||||
showModal(idx) {
|
||||
this.current = idx
|
||||
this.displayResizedImage()
|
||||
this.displayImage()
|
||||
this.modal = true
|
||||
},
|
||||
closeModal() {
|
||||
|
@ -68,11 +88,11 @@ export default {
|
|||
},
|
||||
showPrevious() {
|
||||
this.current--
|
||||
this.displayResizedImage()
|
||||
this.displayImage()
|
||||
},
|
||||
showNext() {
|
||||
this.current++
|
||||
this.displayResizedImage()
|
||||
this.displayImage()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
-->
|
||||
|
||||
<template>
|
||||
<div v-if="account && accountInfo" class="user-profile">
|
||||
<div v-if="profileAccount && accountInfo" class="user-profile">
|
||||
<div>
|
||||
<avatar v-if="accountInfo.local" :user="localUid" :disable-tooltip="true"
|
||||
:size="128" />
|
||||
|
@ -34,7 +34,7 @@
|
|||
{{ accountInfo.website.value }}
|
||||
</a>
|
||||
</p>
|
||||
<follow-button :account="accountInfo.account" />
|
||||
<follow-button :account="accountInfo.account" :uid="uid" />
|
||||
<button v-if="serverData.public" class="primary" @click="followRemote">
|
||||
{{ t('social', 'Follow') }}
|
||||
</button>
|
||||
|
@ -97,6 +97,7 @@
|
|||
</style>
|
||||
<script>
|
||||
import Avatar from '@nextcloud/vue/dist/Components/Avatar'
|
||||
import accountMixins from '../mixins/accountMixins'
|
||||
import serverData from '../mixins/serverData'
|
||||
import currentUser from '../mixins/currentUserMixin'
|
||||
import follow from '../mixins/follow'
|
||||
|
@ -110,8 +111,9 @@ export default {
|
|||
Avatar
|
||||
},
|
||||
mixins: [
|
||||
serverData,
|
||||
accountMixins,
|
||||
currentUser,
|
||||
serverData,
|
||||
follow
|
||||
],
|
||||
props: {
|
||||
|
@ -130,9 +132,6 @@ export default {
|
|||
// Returns only the local part of a username
|
||||
return (this.uid.indexOf('@') === -1) ? this.uid : this.uid.substr(0, this.uid.indexOf('@'))
|
||||
},
|
||||
account() {
|
||||
return (this.uid.indexOf('@') === -1) ? this.uid + '@' + this.hostname : this.uid
|
||||
},
|
||||
displayName() {
|
||||
if (typeof this.accountInfo.name !== 'undefined' && this.accountInfo.name !== '') {
|
||||
return this.accountInfo.name
|
||||
|
@ -140,13 +139,11 @@ export default {
|
|||
if (typeof this.accountInfo.preferredUsername !== 'undefined' && this.accountInfo.preferredUsername !== '') {
|
||||
return this.accountInfo.preferredUsername
|
||||
}
|
||||
return this.account
|
||||
},
|
||||
accountInfo: function() {
|
||||
return this.$store.getters.getAccount(this.account)
|
||||
return this.profileAccount
|
||||
},
|
||||
getCount() {
|
||||
return (field) => this.accountInfo.details.count ? this.accountInfo.details.count[field] : ''
|
||||
let account = this.accountInfo
|
||||
return (field) => account.details.count ? account.details.count[field] : ''
|
||||
},
|
||||
avatarUrl() {
|
||||
return generateUrl('/apps/social/api/v1/global/actor/avatar?id=' + this.accountInfo.id)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div class="timeline-entry" @click="getSinglePostTimeline">
|
||||
<div class="timeline-entry">
|
||||
<div v-if="item.type === 'SocialAppNotification'">
|
||||
{{ actionSummary }}
|
||||
</div>
|
||||
|
@ -30,7 +30,6 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import Logger from '../logger'
|
||||
import TimelinePost from './TimelinePost.vue'
|
||||
|
||||
export default {
|
||||
|
@ -95,35 +94,6 @@ export default {
|
|||
}
|
||||
},
|
||||
methods: {
|
||||
getSinglePostTimeline(e) {
|
||||
|
||||
// Do not call the single-post view when clicking on a link, a post attachment miniature or the post's author
|
||||
if (e.target.tagName === 'A' || e.target.tagName === 'IMG' || e.target.className.startsWith('post-author')) {
|
||||
Logger.debug('will not call single-post', { event: e })
|
||||
return
|
||||
}
|
||||
|
||||
// Display internal or external post
|
||||
if (!this.item.local) {
|
||||
if (this.item.type === 'Note') {
|
||||
window.open(this.item.id)
|
||||
} else if (this.item.type === 'Announce') {
|
||||
window.open(this.item.object)
|
||||
} else {
|
||||
Logger.warn("Don't know what to do with posts of type " + this.item.type, { post: this.item })
|
||||
}
|
||||
} else {
|
||||
this.$router.push({ name: 'single-post',
|
||||
params: {
|
||||
account: this.item.actor_info.preferredUsername,
|
||||
id: this.item.id,
|
||||
localId: this.item.id.split('/')[this.item.id.split('/').length - 1],
|
||||
type: 'single-post'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
},
|
||||
userDisplayName(actorInfo) {
|
||||
return actorInfo.name !== '' ? actorInfo.name : actorInfo.preferredUsername
|
||||
}
|
||||
|
|
|
@ -60,6 +60,7 @@ import InfiniteLoading from 'vue-infinite-loading'
|
|||
import TimelineEntry from './TimelineEntry.vue'
|
||||
import CurrentUserMixin from './../mixins/currentUserMixin'
|
||||
import EmptyContent from './EmptyContent.vue'
|
||||
import Logger from '../logger.js'
|
||||
|
||||
export default {
|
||||
name: 'Timeline',
|
||||
|
@ -113,6 +114,9 @@ export default {
|
|||
tags: {
|
||||
image: 'img/undraw/profile.svg',
|
||||
title: t('social', 'No posts found for this tag')
|
||||
},
|
||||
'single-post': {
|
||||
title: t('social', 'No replies found')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -122,6 +126,7 @@ export default {
|
|||
if (typeof this.emptyContent[this.$route.params.type] !== 'undefined') {
|
||||
return this.emptyContent[this.$route.params.type]
|
||||
}
|
||||
|
||||
if (typeof this.emptyContent[this.$route.name] !== 'undefined') {
|
||||
let content = this.emptyContent[this.$route.name]
|
||||
// Change text on profile page when accessed by another user or a public (non-authenticated) user
|
||||
|
@ -130,7 +135,9 @@ export default {
|
|||
}
|
||||
return this.$route.name === 'timeline' ? this.emptyContent['default'] : content
|
||||
}
|
||||
|
||||
// Fallback
|
||||
Logger.log('Did not find any empty content for this route', { 'routeType': this.$route.params.type, 'routeName': this.$route.name })
|
||||
return this.emptyContent.default
|
||||
},
|
||||
timeline: function() {
|
||||
|
|
|
@ -26,9 +26,9 @@
|
|||
</span>
|
||||
</a>
|
||||
</div>
|
||||
<div :data-timestamp="timestamp" class="post-timestamp live-relative-timestamp">
|
||||
<a :data-timestamp="timestamp" class="post-timestamp live-relative-timestamp" @click="getSinglePostTimeline">
|
||||
{{ relativeTimestamp }}
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||
<div v-if="item.content" class="post-message">
|
||||
|
@ -64,6 +64,7 @@ import 'linkifyjs/string'
|
|||
import popoverMenu from './../mixins/popoverMenu'
|
||||
import currentUser from './../mixins/currentUserMixin'
|
||||
import PostAttachment from './PostAttachment.vue'
|
||||
import Logger from '../logger'
|
||||
import MessageContent from './MessageContent'
|
||||
import moment from '@nextcloud/moment'
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
|
@ -140,10 +141,36 @@ export default {
|
|||
}
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* @function getSinglePostTimeline
|
||||
* @description Opens the timeline of the post clicked
|
||||
*/
|
||||
getSinglePostTimeline(e) {
|
||||
// Display internal or external post
|
||||
if (!this.item.local) {
|
||||
if (this.item.type === 'Note') {
|
||||
window.open(this.item.id)
|
||||
} else if (this.item.type === 'Announce') {
|
||||
window.open(this.item.object)
|
||||
} else {
|
||||
Logger.warn("Don't know what to do with posts of type " + this.item.type, { post: this.item })
|
||||
}
|
||||
} else {
|
||||
this.$router.push({ name: 'single-post',
|
||||
params: {
|
||||
account: this.item.actor_info.preferredUsername,
|
||||
id: this.item.id,
|
||||
localId: this.item.id.split('/')[this.item.id.split('/').length - 1],
|
||||
type: 'single-post'
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
userDisplayName(actorInfo) {
|
||||
return actorInfo.name !== '' ? actorInfo.name : actorInfo.preferredUsername
|
||||
},
|
||||
reply() {
|
||||
this.$store.commit('setComposerDisplayStatus', true)
|
||||
this.$root.$emit('composer-reply', this.item)
|
||||
},
|
||||
boost() {
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* @copyright Copyright (c) 2019 Cyrille Bollu <cyrpub@bollu.be>
|
||||
*
|
||||
* @author Cyrille Bollu <cyrpub@bollu.be>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* @file provides global account related methods
|
||||
*
|
||||
* @mixin
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
import serverData from './serverData'
|
||||
|
||||
export default {
|
||||
mixins: [
|
||||
serverData
|
||||
],
|
||||
computed: {
|
||||
/** @function Returns the complete account name */
|
||||
profileAccount() {
|
||||
return (this.uid.indexOf('@') === -1) ? this.uid + '@' + this.hostname : this.uid
|
||||
},
|
||||
/** @functions Returns detailed information about an account (account must be loaded in the store first) */
|
||||
accountInfo() {
|
||||
return this.$store.getters.getAccount(this.profileAccount)
|
||||
},
|
||||
/** @function Somewhat duplicate with accountInfo(), but needed (for some reason) to avoid glitches
|
||||
* where components would first show "user not found" before display an account's account info */
|
||||
accountLoaded() {
|
||||
return this.$store.getters.accountLoaded(this.profileAccount)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,6 +1,10 @@
|
|||
/*
|
||||
* @copyright Copyright (c) 2018 Julius Härtl <jus@bitgrid.net>
|
||||
*
|
||||
* @file Provides global methods for using the serverData structure.
|
||||
*
|
||||
* @mixin
|
||||
*
|
||||
* @author Julius Härtl <jus@bitgrid.net>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
|
@ -22,7 +26,17 @@
|
|||
|
||||
export default {
|
||||
computed: {
|
||||
serverData: function() {
|
||||
/** @description Returns the serverData object
|
||||
* @property {String} account - The account that the user wants to follow (Only in 'OStatus.vue')
|
||||
* @property cliUrl
|
||||
* @property cloudAddress
|
||||
* @property firstrun
|
||||
* @property isAdmin
|
||||
* @property {String} local - The local part of the account that the user wants to follow
|
||||
* @property {boolean} public - False when the page is accessed by an authenticated user. True otherwise
|
||||
* @property setup
|
||||
*/
|
||||
serverData() {
|
||||
return this.$store.getters.getServerData
|
||||
},
|
||||
hostname() {
|
||||
|
|
|
@ -46,13 +46,10 @@ const mutations = {
|
|||
let users = []
|
||||
for (var index in data) {
|
||||
const actor = data[index].actor_info
|
||||
if (typeof actor !== 'undefined' && account !== actor.account) {
|
||||
users.push(actor.id)
|
||||
addAccount(state, {
|
||||
actorId: actor.id,
|
||||
data: actor
|
||||
})
|
||||
}
|
||||
addAccount(state, {
|
||||
actorId: actor.id,
|
||||
data: actor
|
||||
})
|
||||
}
|
||||
Vue.set(state.accounts[_getActorIdForAccount(account)], 'followersList', users)
|
||||
},
|
||||
|
@ -79,6 +76,9 @@ const mutations = {
|
|||
}
|
||||
|
||||
const getters = {
|
||||
getAllAccounts(state) {
|
||||
return (account) => { return state.accounts }
|
||||
},
|
||||
getAccount(state, getters) {
|
||||
return (account) => {
|
||||
return state.accounts[_getActorIdForAccount(account)]
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
/*
|
||||
* @copyright Copyright (c) 2018 Julius Härtl <jus@bitgrid.net>
|
||||
*
|
||||
* @file Timeline related store
|
||||
*
|
||||
* @author Julius Härtl <jus@bitgrid.net>
|
||||
* @author Jonas Sulzer <jonas@violoncello.ch>
|
||||
*
|
||||
|
@ -26,12 +28,31 @@ import axios from '@nextcloud/axios'
|
|||
import Vue from 'vue'
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
|
||||
/**
|
||||
* @property {object} timeline - The posts' collection
|
||||
* @property {int} since - Time (EPOCH) of the most recent post
|
||||
* @property {string} type - Timeline's type: 'home', 'single-post',...
|
||||
* @property {object} params - Timeline's parameters
|
||||
* @property {string} account -
|
||||
*/
|
||||
const state = {
|
||||
timeline: {},
|
||||
since: Math.floor(Date.now() / 1000) + 1,
|
||||
type: 'home',
|
||||
/**
|
||||
* @namespace params
|
||||
* @property {string} account ???
|
||||
* @property {string} id
|
||||
* @property {string} localId
|
||||
* @property {string} type ???
|
||||
*/
|
||||
params: {},
|
||||
account: ''
|
||||
account: '',
|
||||
/* Tells whether the composer should be displayed or not.
|
||||
* It's up to the view to honor this status or not.
|
||||
* @member {boolean}
|
||||
*/
|
||||
composerDisplayStatus: false
|
||||
}
|
||||
const mutations = {
|
||||
addToTimeline(state, data) {
|
||||
|
@ -53,6 +74,9 @@ const mutations = {
|
|||
setTimelineParams(state, params) {
|
||||
state.params = params
|
||||
},
|
||||
setComposerDisplayStatus(state, status) {
|
||||
state.composerDisplayStatus = status
|
||||
},
|
||||
setAccount(state, account) {
|
||||
state.account = account
|
||||
},
|
||||
|
@ -90,6 +114,9 @@ const mutations = {
|
|||
}
|
||||
}
|
||||
const getters = {
|
||||
getComposerDisplayStatus(state) {
|
||||
return state.composerDisplayStatus
|
||||
},
|
||||
getTimeline(state) {
|
||||
return Object.values(state.timeline).sort(function(a, b) {
|
||||
return b.publishedTime - a.publishedTime
|
||||
|
@ -120,12 +147,11 @@ const actions = {
|
|||
post(context, post) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios.post(generateUrl('apps/social/api/v1/post'), { data: post }).then((response) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Post created with token ' + response.data.result.token)
|
||||
Logger.info('Post created with token ' + response.data.result.token)
|
||||
resolve(response)
|
||||
}).catch((error) => {
|
||||
OC.Notification.showTemporary('Failed to create a post')
|
||||
console.error('Failed to create a post', error.response)
|
||||
Logger.error('Failed to create a post', { 'error': error.response })
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
|
@ -133,11 +159,10 @@ const actions = {
|
|||
postDelete(context, post) {
|
||||
return axios.delete(generateUrl(`apps/social/api/v1/post?id=${post.id}`)).then((response) => {
|
||||
context.commit('removePost', post)
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Post deleted with token ' + response.data.result.token)
|
||||
Logger.info('Post deleted with token ' + response.data.result.token)
|
||||
}).catch((error) => {
|
||||
OC.Notification.showTemporary('Failed to delete the post')
|
||||
console.error('Failed to delete the post', error)
|
||||
Logger.error('Failed to delete the post', { 'error': error })
|
||||
})
|
||||
},
|
||||
postLike(context, { post, parentAnnounce }) {
|
||||
|
@ -147,7 +172,7 @@ const actions = {
|
|||
resolve(response)
|
||||
}).catch((error) => {
|
||||
OC.Notification.showTemporary('Failed to like post')
|
||||
console.error('Failed to like post', error.response)
|
||||
Logger.error('Failed to like post', { 'error': error.response })
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
|
@ -161,19 +186,18 @@ const actions = {
|
|||
}
|
||||
}).catch((error) => {
|
||||
OC.Notification.showTemporary('Failed to unlike post')
|
||||
console.error('Failed to unlike post', error)
|
||||
Logger.error('Failed to unlike post', { 'error': error })
|
||||
})
|
||||
},
|
||||
postBoost(context, { post, parentAnnounce }) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios.post(generateUrl(`apps/social/api/v1/post/boost?postId=${post.id}`)).then((response) => {
|
||||
context.commit('boostPost', { post, parentAnnounce })
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Post boosted with token ' + response.data.result.token)
|
||||
Logger.info('Post boosted with token ' + response.data.result.token)
|
||||
resolve(response)
|
||||
}).catch((error) => {
|
||||
OC.Notification.showTemporary('Failed to create a boost post')
|
||||
console.error('Failed to create a boost post', error.response)
|
||||
Logger.error('Failed to create a boost post', { 'error': error.response })
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
|
@ -181,11 +205,10 @@ const actions = {
|
|||
postUnBoost(context, { post, parentAnnounce }) {
|
||||
return axios.delete(generateUrl(`apps/social/api/v1/post/boost?postId=${post.id}`)).then((response) => {
|
||||
context.commit('unboostPost', { post, parentAnnounce })
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Boost deleted with token ' + response.data.result.token)
|
||||
Logger.info('Boost deleted with token ' + response.data.result.token)
|
||||
}).catch((error) => {
|
||||
OC.Notification.showTemporary('Failed to delete the boost')
|
||||
console.error('Failed to delete the boost', error)
|
||||
Logger.error('Failed to delete the boost', { 'error': error })
|
||||
})
|
||||
},
|
||||
refreshTimeline(context) {
|
||||
|
@ -216,22 +239,8 @@ const actions = {
|
|||
throw response.message
|
||||
}
|
||||
|
||||
let result = []
|
||||
|
||||
// Also load replies when displaying a single post timeline
|
||||
if (state.type === 'single-post') {
|
||||
result.push(response.data)
|
||||
// axios.get(generateUrl(``)).then((response) => {
|
||||
// if (response.status !== -1) {
|
||||
// result.concat(response.data.result)
|
||||
// }
|
||||
// }
|
||||
} else {
|
||||
result = response.data.result
|
||||
}
|
||||
|
||||
// Add results to timeline
|
||||
context.commit('addToTimeline', result)
|
||||
context.commit('addToTimeline', response.data.result)
|
||||
|
||||
return response.data
|
||||
})
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div v-if="accountInfo">
|
||||
<div v-if="account">
|
||||
<div v-if="!serverData.local">
|
||||
<h2>{{ t('social', 'Follow on Nextcloud Social') }}</h2>
|
||||
<p>{{ t('social', 'Hello') }} <avatar :user="currentUser.uid" :size="16" />{{ currentUser.displayName }}</p>
|
||||
|
@ -23,6 +23,7 @@
|
|||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Some unauthenticated user wants to follow a local account -->
|
||||
<div v-if="serverData.local">
|
||||
<p>{{ t('social', 'You are going to follow:') }}</p>
|
||||
<avatar :user="serverData.local" :disable-tooltip="true" :size="128" />
|
||||
|
@ -34,7 +35,7 @@
|
|||
<p>{{ t('social', 'This step is needed as the user is probably not registered on the same server as you are. We will redirect you to your homeserver to follow this account.') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else :class="{ 'icon-loading-dark': !accountInfo }" />
|
||||
<div v-else :class="{ 'icon-loading-dark': !account }" />
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
@ -62,7 +63,8 @@
|
|||
<script>
|
||||
import Avatar from '@nextcloud/vue/dist/Components/Avatar'
|
||||
import axios from '@nextcloud/axios'
|
||||
import currentuserMixin from './../mixins/currentUserMixin'
|
||||
import accountMixins from '../mixins/accountMixins'
|
||||
import currentuserMixin from '../mixins/currentUserMixin'
|
||||
import { loadState } from '@nextcloud/initial-state'
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
|
||||
|
@ -71,37 +73,36 @@ export default {
|
|||
components: {
|
||||
Avatar
|
||||
},
|
||||
mixins: [currentuserMixin],
|
||||
mixins: [
|
||||
accountMixins,
|
||||
currentuserMixin
|
||||
],
|
||||
data() {
|
||||
return {
|
||||
remote: ''
|
||||
remote: '',
|
||||
account: {}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isFollowing() {
|
||||
return this.$store.getters.isFollowingUser(this.account)
|
||||
},
|
||||
account() {
|
||||
return this.serverData.account
|
||||
return this.$store.getters.isFollowingUser(this.account.id)
|
||||
},
|
||||
avatarUrl() {
|
||||
return generateUrl('/apps/social/api/v1/global/actor/avatar?id=' + this.accountInfo.id)
|
||||
},
|
||||
accountInfo: function() {
|
||||
return this.$store.getters.getAccount(this.serverData.account)
|
||||
return generateUrl('/apps/social/api/v1/global/actor/avatar?id=' + this.account.id)
|
||||
},
|
||||
currentUser() {
|
||||
return window.oc_current_user
|
||||
},
|
||||
displayName() {
|
||||
if (typeof this.accountInfo.name !== 'undefined' && this.accountInfo.name !== '') {
|
||||
return this.accountInfo.name
|
||||
if (typeof this.account.id === 'undefined') {
|
||||
return (this.serverData.account ? this.serverData.account : this.serverData.local)
|
||||
}
|
||||
return this.account
|
||||
|
||||
return (this.account.name ? this.account.name : this.account.preferredUsername)
|
||||
}
|
||||
},
|
||||
beforeMount: function() {
|
||||
// importing server data into the store
|
||||
// importing server data into the store and fetching viewed account's information
|
||||
try {
|
||||
const serverData = loadState('social', 'serverData')
|
||||
if (serverData.currentUser) {
|
||||
|
@ -109,10 +110,14 @@ export default {
|
|||
}
|
||||
this.$store.commit('setServerData', serverData)
|
||||
if (this.serverData.account && !this.serverData.local) {
|
||||
this.$store.dispatch('fetchAccountInfo', this.serverData.account)
|
||||
this.$store.dispatch('fetchAccountInfo', this.serverData.account).then((result) => {
|
||||
this.account = result
|
||||
})
|
||||
}
|
||||
if (this.serverData.local) {
|
||||
this.$store.dispatch('fetchPublicAccountInfo', this.serverData.local)
|
||||
this.$store.dispatch('fetchPublicAccountInfo', this.serverData.local).then((result) => {
|
||||
this.account = result
|
||||
})
|
||||
}
|
||||
} catch {
|
||||
/* empty */
|
||||
|
@ -120,7 +125,7 @@ export default {
|
|||
},
|
||||
methods: {
|
||||
follow() {
|
||||
this.$store.dispatch('followAccount', { currentAccount: this.cloudId, accountToFollow: this.account }).then(() => {
|
||||
this.$store.dispatch('followAccount', { currentAccount: this.cloudId, accountToFollow: this.account.account }).then(() => {
|
||||
|
||||
})
|
||||
},
|
||||
|
|
|
@ -40,6 +40,7 @@
|
|||
<script>
|
||||
import ProfileInfo from './../components/ProfileInfo.vue'
|
||||
import EmptyContent from '../components/EmptyContent.vue'
|
||||
import accountMixins from '../mixins/accountMixins'
|
||||
import serverData from '../mixins/serverData'
|
||||
|
||||
export default {
|
||||
|
@ -49,6 +50,7 @@ export default {
|
|||
ProfileInfo
|
||||
},
|
||||
mixins: [
|
||||
accountMixins,
|
||||
serverData
|
||||
],
|
||||
data() {
|
||||
|
@ -58,18 +60,9 @@ export default {
|
|||
}
|
||||
},
|
||||
computed: {
|
||||
profileAccount() {
|
||||
return (this.uid.indexOf('@') === -1) ? this.uid + '@' + this.hostname : this.uid
|
||||
},
|
||||
timeline: function() {
|
||||
return this.$store.getters.getTimeline
|
||||
},
|
||||
accountInfo: function() {
|
||||
return this.$store.getters.getAccount(this.profileAccount)
|
||||
},
|
||||
accountLoaded() {
|
||||
return this.$store.getters.accountLoaded(this.profileAccount)
|
||||
},
|
||||
emptyContentData() {
|
||||
return {
|
||||
image: 'img/undraw/profile.svg',
|
||||
|
@ -78,12 +71,12 @@ export default {
|
|||
}
|
||||
}
|
||||
},
|
||||
// Start fetching account information before mounting the component
|
||||
beforeMount() {
|
||||
|
||||
let fetchMethod = ''
|
||||
this.uid = this.$route.params.account || this.serverData.account
|
||||
|
||||
// Are we authenticated?
|
||||
let fetchMethod = ''
|
||||
if (this.serverData.public) {
|
||||
fetchMethod = 'fetchPublicAccountInfo'
|
||||
} else {
|
||||
|
@ -95,8 +88,6 @@ export default {
|
|||
this.$store.dispatch(fetchMethod, this.profileAccount).then((response) => {
|
||||
this.uid = response.account
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
<template>
|
||||
<div class="social__wrapper">
|
||||
<timeline-entry :item="mainPost" />
|
||||
<timeline-list :type="$route.params.type" />
|
||||
<profile-info v-if="accountLoaded && accountInfo" :uid="uid" />
|
||||
<composer v-show="composerDisplayStatus" />
|
||||
<timeline-entry class="main-post" :item="mainPost" />
|
||||
<timeline-list v-if="timeline" :type="$route.params.type" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -19,44 +21,80 @@
|
|||
</style>
|
||||
|
||||
<script>
|
||||
import Logger from '../logger'
|
||||
import TimelineEntry from './../components/TimelineEntry.vue'
|
||||
import TimelineList from './../components/TimelineList.vue'
|
||||
import Composer from '../components/Composer.vue'
|
||||
import ProfileInfo from '../components/ProfileInfo.vue'
|
||||
import TimelineEntry from '../components/TimelineEntry.vue'
|
||||
import TimelineList from '../components/TimelineList.vue'
|
||||
import currentUserMixin from '../mixins/currentUserMixin'
|
||||
import accountMixins from '../mixins/accountMixins'
|
||||
import serverData from '../mixins/serverData'
|
||||
import { loadState } from '@nextcloud/initial-state'
|
||||
|
||||
export default {
|
||||
name: 'TimelineSinglePost',
|
||||
components: {
|
||||
Composer,
|
||||
ProfileInfo,
|
||||
TimelineEntry,
|
||||
TimelineList
|
||||
},
|
||||
mixins: [
|
||||
accountMixins,
|
||||
currentUserMixin,
|
||||
serverData
|
||||
],
|
||||
data() {
|
||||
return {
|
||||
mainPost: {}
|
||||
mainPost: {},
|
||||
uid: this.$route.params.account
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
/**
|
||||
* @description Tells whether Composer shall be displayed or not
|
||||
* @returns {boolean}
|
||||
*/
|
||||
composerDisplayStatus() {
|
||||
return this.$store.getters.getComposerDisplayStatus
|
||||
},
|
||||
/**
|
||||
* @description Extracts the viewed account name from the URL
|
||||
* @returns {String}
|
||||
*/
|
||||
account() {
|
||||
return window.location.href.split('/')[window.location.href.split('/').length - 2].substr(1)
|
||||
},
|
||||
/**
|
||||
* @description Returns the timeline currently loaded in the store
|
||||
* @returns {Object}
|
||||
*/
|
||||
timeline: function() {
|
||||
return this.$store.getters.getTimeline
|
||||
}
|
||||
},
|
||||
beforeMount: function() {
|
||||
|
||||
// Get data of post clicked on
|
||||
if (typeof this.$route.params.id === 'undefined') {
|
||||
Logger.debug('displaying the single post timeline for a non logged-in user')
|
||||
this.mainPost = loadState('social', 'item')
|
||||
} else {
|
||||
this.mainPost = this.$store.getters.getPostFromTimeline(this.$route.params.id)
|
||||
}
|
||||
|
||||
// Set params for the TimelineList component
|
||||
// Fetch information of the related account
|
||||
this.$store.dispatch(this.serverData.public ? 'fetchPublicAccountInfo' : 'fetchAccountInfo', this.account).then((response) => {
|
||||
// We need to update this.uid because we may have asked info for an account whose domain part was a host-meta,
|
||||
// and the account returned by the backend always uses a non host-meta'ed domain for its ID
|
||||
this.uid = response.account
|
||||
})
|
||||
|
||||
// Fetch single post timeline
|
||||
let params = {
|
||||
account: window.location.href.split('/')[window.location.href.split('/').length - 2].substr(1),
|
||||
account: this.account,
|
||||
id: window.location.href,
|
||||
localId: window.location.href.split('/')[window.location.href.split('/').length - 1],
|
||||
type: 'single-post'
|
||||
}
|
||||
|
||||
this.$store.dispatch('changeTimelineType', {
|
||||
type: 'single-post',
|
||||
params: params
|
||||
|
@ -66,3 +104,7 @@ export default {
|
|||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
</style>
|
||||
|
|
Ładowanie…
Reference in New Issue