Merge pull request #546 from nextcloud/boosts

display boosted posts with an according indication
pull/549/head
Maxence Lange 2019-05-29 22:44:02 -01:00 zatwierdzone przez GitHub
commit b8d1546be7
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
10 zmienionych plików z 337 dodań i 202 usunięć

8
composer.lock wygenerowano
Wyświetl plik

@ -12,12 +12,12 @@
"source": {
"type": "git",
"url": "https://github.com/daita/my-small-php-tools.git",
"reference": "732d54bca742e3ecdb2b544589550a37172c1258"
"reference": "6e8f346a2ee488655316d1e4139c27417d6b7e4d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/daita/my-small-php-tools/zipball/732d54bca742e3ecdb2b544589550a37172c1258",
"reference": "732d54bca742e3ecdb2b544589550a37172c1258",
"url": "https://api.github.com/repos/daita/my-small-php-tools/zipball/6e8f346a2ee488655316d1e4139c27417d6b7e4d",
"reference": "6e8f346a2ee488655316d1e4139c27417d6b7e4d",
"shasum": ""
},
"require": {
@ -40,7 +40,7 @@
}
],
"description": "My small PHP Tools",
"time": "2019-05-27T17:53:41+00:00"
"time": "2019-05-29T20:52:05+00:00"
},
{
"name": "friendica/json-ld",

Wyświetl plik

@ -731,8 +731,12 @@ class CoreRequestBuilder {
->selectAlias('sa.stream_id', 'streamaction_stream_id')
->selectAlias('sa.values', 'streamaction_values');
$orX = $expr->orX();
$orX->add($expr->eq($func->lower($pf . '.id'), $func->lower('sa.stream_id')));
$orX->add($expr->eq($func->lower($pf . '.object_id'), $func->lower('sa.stream_id')));
$andX = $expr->andX();
$andX->add($expr->eq($func->lower($pf . '.id'), $func->lower('sa.stream_id')));
$andX->add($orX);
$andX->add(
$expr->eq(
$func->lower('sa.actor_id'),
@ -751,7 +755,6 @@ class CoreRequestBuilder {
* @param array $data
*
* @return StreamAction
* @throws InvalidResourceException
*/
protected function parseStreamActionsLeftJoin(array $data): StreamAction {
$new = [];
@ -763,10 +766,11 @@ class CoreRequestBuilder {
$action = new StreamAction();
$action->importFromDatabase($new);
if ($action->getId() === 0) {
throw new InvalidResourceException();
}
$action->setDefaultValues(
[
'boosted' => false
]
);
return $action;
}

Wyświetl plik

@ -524,7 +524,7 @@ class StreamRequest extends StreamRequestBuilder {
}
$cache = '[]';
if ($stream->gotCache()) {
if ($stream->hasCache()) {
$cache = json_encode($stream->getCache(), JSON_UNESCAPED_SLASHES);
}

Wyświetl plik

@ -30,6 +30,7 @@ declare(strict_types=1);
namespace OCA\Social\Db;
use daita\MySmallPhpTools\Exceptions\CacheItemNotFoundException;
use daita\MySmallPhpTools\Traits\TArrayTools;
use Doctrine\DBAL\Query\QueryBuilder;
use OCA\Social\AP;
@ -415,12 +416,21 @@ class StreamRequestBuilder extends CoreRequestBuilder {
} catch (InvalidResourceException $e) {
}
try {
$action = $this->parseStreamActionsLeftJoin($data);
$item->setAction($action);
} catch (InvalidResourceException $e) {
$action = $this->parseStreamActionsLeftJoin($data);
if ($item->hasCache()) {
$cache = $item->getCache();
try {
$cachedItem = $cache->getItem($action->getStreamId());
$cachedObject = $cachedItem->getObject();
$cachedObject['action'] = $action;
$cachedItem->setContent(json_encode($cachedObject));
$cache->updateItem($cachedItem, false);
} catch (CacheItemNotFoundException $e) {
}
}
$item->setAction($action);
return $item;
}

Wyświetl plik

@ -231,7 +231,7 @@ class Stream extends ACore implements JsonSerializable {
/**
* @return bool
*/
public function gotCache(): bool {
public function hasCache(): bool {
return ($this->cache !== null);
}
@ -257,7 +257,7 @@ class Stream extends ACore implements JsonSerializable {
public function addCacheItem(string $url): Stream {
$cacheItem = new CacheItem($url);
if (!$this->gotCache()) {
if (!$this->hasCache()) {
$this->setCache(new Cache());
}
@ -376,7 +376,7 @@ class Stream extends ACore implements JsonSerializable {
$result,
[
'action' => ($this->hasAction()) ? $this->getAction() : [],
'cache' => ($this->gotCache()) ? $this->getCache() : '',
'cache' => ($this->hasCache()) ? $this->getCache() : '',
'publishedTime' => $this->getPublishedTime()
]
);

Wyświetl plik

@ -209,6 +209,23 @@ class StreamAction implements JsonSerializable {
}
/**
* @param array $default
*
* @return StreamAction
*/
public function setDefaultValues(array $default): StreamAction {
$keys = array_keys($default);
foreach ($keys as $k) {
if (!array_key_exists($k, $this->values)) {
$this->values[$k] = $default[$k];
}
}
return $this;
}
/**
* @param array $data
*/

Wyświetl plik

@ -177,7 +177,7 @@ class StreamQueueService {
return;
}
if (!$stream->gotCache()) {
if (!$stream->hasCache()) {
$this->deleteCache($queue);
return;

Wyświetl plik

@ -0,0 +1,220 @@
<template>
<div class="entry-content">
<div v-if="item.actor_info" class="post-avatar">
<avatar v-if="item.local" :size="32" :user="item.actor_info.preferredUsername"
:display-name="item.actor_info.account" :disable-tooltip="true" />
<avatar v-else :size="32" :url="avatarUrl"
:disable-tooltip="true" />
</div>
<div class="post-content">
<div class="post-author-wrapper">
<router-link v-if="item.actor_info" :to="{ name: 'profile', params: { account: item.local ? item.actor_info.preferredUsername : item.actor_info.account }}">
<span class="post-author">
{{ userDisplayName(item.actor_info) }}
</span>
<span class="post-author-id">
@{{ item.actor_info.account }}
</span>
</router-link>
<a v-else :href="item.attributedTo">
<span class="post-author-id">
{{ item.attributedTo }}
</span>
</a>
</div>
<!-- eslint-disable-next-line vue/no-v-html -->
<div class="post-message" v-html="formatedMessage" />
<div v-click-outside="hidePopoverMenu" class="post-actions">
<a v-tooltip.bottom="t('social', 'Reply')" class="icon-reply" @click.prevent="reply" />
<a v-if="item.actor_info.account !== cloudId" v-tooltip.bottom="t('social', 'Boost')"
:class="(isBoosted) ? 'icon-boosted' : 'icon-boost'"
@click.prevent="boost" />
<div v-if="popoverMenu.length > 0" v-tooltip.bottom="t('social', 'More actions')" class="post-actions-more">
<a class="icon-more" @click.prevent="togglePopoverMenu" />
<div :class="{open: menuOpened}" class="popovermenu menu-center">
<popover-menu :menu="popoverMenu" />
</div>
</div>
</div>
</div>
<div>
<div :data-timestamp="timestamp" class="post-timestamp live-relative-timestamp">
{{ relativeTimestamp }}
</div>
</div>
</div>
</template>
<script>
import Avatar from 'nextcloud-vue/dist/Components/Avatar'
import * as linkify from 'linkifyjs'
import pluginTag from 'linkifyjs/plugins/hashtag'
import pluginMention from 'linkifyjs/plugins/mention'
import 'linkifyjs/string'
import popoverMenu from './../mixins/popoverMenu'
import currentUser from './../mixins/currentUserMixin'
pluginTag(linkify)
pluginMention(linkify)
export default {
name: 'TimelineContent',
components: {
Avatar
},
mixins: [popoverMenu, currentUser],
props: {
item: { type: Object, default: () => {} },
parentAnnounce: { type: Object, default: () => {} }
},
data() {
return {
}
},
computed: {
popoverMenu() {
var actions = [
]
if (this.item.actor_info.account === this.cloudId) {
actions.push(
{
action: () => {
this.$store.dispatch('postDelete', this.item)
this.hidePopoverMenu()
},
icon: 'icon-delete',
text: t('social', 'Delete post')
}
)
}
return actions
},
relativeTimestamp() {
return OC.Util.relativeModifiedDate(this.item.published)
},
timestamp() {
return Date.parse(this.item.published)
},
formatedMessage() {
let message = this.item.content
if (typeof message === 'undefined') {
return ''
}
message = message.replace(/(?:\r\n|\r|\n)/g, '<br />')
message = message.linkify({
formatHref: {
hashtag: function(href) {
return OC.generateUrl('/apps/social/timeline/tags/' + href.substring(1))
},
mention: function(href) {
return OC.generateUrl('/apps/social/@' + href.substring(1))
}
}
})
message = this.$twemoji.parse(message)
return message
},
avatarUrl() {
return OC.generateUrl('/apps/social/api/v1/global/actor/avatar?id=' + this.item.attributedTo)
},
isBoosted() {
if (typeof this.item.action === 'undefined') {
return false
}
return !!this.item.action.values.boosted
}
},
methods: {
userDisplayName(actorInfo) {
return actorInfo.name !== '' ? actorInfo.name : actorInfo.preferredUsername
},
reply() {
this.$root.$emit('composer-reply', this.item)
},
boost() {
let params = {
post: this.item,
parentAnnounce: this.parentAnnounce
}
if (this.isBoosted) {
this.$store.dispatch('postUnBoost', params)
} else {
this.$store.dispatch('postBoost', params)
}
}
}
}
</script>
<style scoped lang="scss">
.post-author {
font-weight: bold;
}
.post-author-id {
opacity: .7;
}
.post-avatar {
margin: 5px;
margin-right: 10px;
border-radius: 50%;
overflow: hidden;
width: 32px;
height: 32px;
min-width: 32px;
flex-shrink: 0;
}
.post-timestamp {
width: 120px;
text-align: right;
flex-shrink: 0;
}
.post-actions {
margin-left: -13px;
height: 44px;
.post-actions-more {
position: relative;
width: 44px;
height: 34px;
display: inline-block;
}
.icon-reply,
.icon-boost,
.icon-boosted,
.icon-more {
display: inline-block;
width: 44px;
height: 34px;
opacity: .5;
&:hover, &:focus {
opacity: 1;
}
}
.icon-boosted {
opacity: 1;
}
}
span {
/* opacity: 0.5; */
}
.entry-content {
display: flex;
}
.post-content {
flex-grow: 1;
}
.post-timestamp {
opacity: .7;
}
</style>
<style>
.post-message a {
text-decoration: underline;
}
</style>

Wyświetl plik

@ -1,70 +1,33 @@
<template>
<div class="timeline-entry">
<div class="entry-content">
<div v-if="item.actor_info" class="post-avatar">
<avatar v-if="item.local" :size="32" :user="item.actor_info.preferredUsername"
:display-name="item.actor_info.account" :disable-tooltip="true" />
<avatar v-else :size="32" :url="avatarUrl"
:disable-tooltip="true" />
</div>
<div class="post-content">
<div class="post-author-wrapper">
<router-link v-if="item.actor_info" :to="{ name: 'profile', params: { account: item.local ? item.actor_info.preferredUsername : item.actor_info.account }}">
<span class="post-author">
{{ userDisplayName(item.actor_info) }}
</span>
<span class="post-author-id">
@{{ item.actor_info.account }}
</span>
</router-link>
<a v-else :href="item.attributedTo">
<span class="post-author-id">
{{ item.attributedTo }}
</span>
</a>
</div>
<!-- eslint-disable-next-line vue/no-v-html -->
<div class="post-message" v-html="formatedMessage" />
<div v-click-outside="hidePopoverMenu" class="post-actions">
<a v-tooltip.bottom="t('social', 'Reply')" class="icon-reply" @click.prevent="reply" />
<a v-if="item.actor_info.account !== cloudId" v-tooltip.bottom="t('social', 'Boost')"
:class="(isBoosted) ? 'icon-boosted' : 'icon-boost'"
@click.prevent="boost" />
<div v-if="popoverMenu.length > 0" v-tooltip.bottom="t('social', 'More actions')" class="post-actions-more">
<a class="icon-more" @click.prevent="togglePopoverMenu" />
<div :class="{open: menuOpened}" class="popovermenu menu-center">
<popover-menu :menu="popoverMenu" />
</div>
</div>
</div>
</div>
<div>
<div :data-timestamp="timestamp" class="post-timestamp live-relative-timestamp">
{{ relativeTimestamp }}
</div>
<div v-if="noDuplicateBoost" class="timeline-entry">
<div v-if="item.type === 'Announce'" class="boost">
<div class="container-icon-boost">
<span class="icon-boost" />
</div>
<router-link v-if="item.actor_info" :to="{ name: 'profile', params: { account: item.local ? item.actor_info.preferredUsername : item.actor_info.account }}">
<span v-tooltip.bottom="item.actor_info.account" class="post-author">
{{ userDisplayName(item.actor_info) }}
</span>
</router-link>
<a v-else :href="item.attributedTo">
<span class="post-author-id">
{{ item.attributedTo }}
</span>
</a>
{{ boosted }}
</div>
<timeline-content :item="entryContent" :parent-announce="isBoost" />
</div>
</template>
<script>
import Avatar from 'nextcloud-vue/dist/Components/Avatar'
import * as linkify from 'linkifyjs'
import pluginTag from 'linkifyjs/plugins/hashtag'
import pluginMention from 'linkifyjs/plugins/mention'
import 'linkifyjs/string'
import popoverMenu from './../mixins/popoverMenu'
import currentUser from './../mixins/currentUserMixin'
pluginTag(linkify)
pluginMention(linkify)
import TimelineContent from './TimelineContent.vue'
export default {
name: 'TimelineEntry',
components: {
Avatar
TimelineContent
},
mixins: [popoverMenu, currentUser],
props: {
item: { type: Object, default: () => {} }
},
@ -73,71 +36,36 @@ export default {
}
},
computed: {
popoverMenu() {
var actions = [
]
if (this.item.actor_info.account === this.cloudId) {
actions.push(
{
action: () => {
this.$store.dispatch('postDelete', this.item)
this.hidePopoverMenu()
},
icon: 'icon-delete',
text: t('social', 'Delete post')
}
)
entryContent() {
if (this.item.type === 'Announce') {
return this.item.cache[this.item.object].object
} else {
return this.item
}
return actions
},
relativeTimestamp() {
return OC.Util.relativeModifiedDate(this.item.published)
},
timestamp() {
return Date.parse(this.item.published)
},
formatedMessage() {
let message = this.item.content
if (typeof message === 'undefined') {
return ''
isBoost() {
if (this.item.type === 'Announce') {
return this.item
}
message = message.replace(/(?:\r\n|\r|\n)/g, '<br />')
message = message.linkify({
formatHref: {
hashtag: function(href) {
return OC.generateUrl('/apps/social/timeline/tags/' + href.substring(1))
},
mention: function(href) {
return OC.generateUrl('/apps/social/@' + href.substring(1))
return {}
},
boosted() {
return t('social', 'boosted')
},
noDuplicateBoost() {
if (this.item.type === 'Announce') {
for (var e in this.$store.state.timeline.timeline) {
if (this.item.cache[this.item.object].object.id === e) {
return false
}
}
})
message = this.$twemoji.parse(message)
return message
},
avatarUrl() {
return OC.generateUrl('/apps/social/api/v1/global/actor/avatar?id=' + this.item.attributedTo)
},
isBoosted() {
if (typeof this.item.action === 'undefined') {
return false
}
return !!this.item.action.values.boosted
return true
}
},
methods: {
userDisplayName(actorInfo) {
return actorInfo.name !== '' ? actorInfo.name : actorInfo.preferredUsername
},
reply() {
this.$root.$emit('composer-reply', this.item)
},
boost() {
if (this.isBoosted) {
this.$store.dispatch('postUnBoost', this.item)
} else {
this.$store.dispatch('postBoost', this.item)
}
}
}
}
@ -148,75 +76,21 @@ export default {
margin-bottom: 10px;
}
.post-author {
font-weight: bold;
.container-icon-boost {
display: inline-block;
padding-right: 6px;
}
.post-author-id {
opacity: .7;
.icon-boost {
display: inline-block;
width: 38px;
height: 17px;
opacity: .5;
background-position: right center;
vertical-align: middle;
}
.post-avatar {
margin: 5px;
margin-right: 10px;
border-radius: 50%;
overflow: hidden;
width: 32px;
height: 32px;
min-width: 32px;
flex-shrink: 0;
}
.post-timestamp {
width: 120px;
text-align: right;
flex-shrink: 0;
}
.post-actions {
margin-left: -13px;
height: 44px;
.post-actions-more {
position: relative;
width: 44px;
height: 34px;
display: inline-block;
}
.icon-reply,
.icon-boost,
.icon-boosted,
.icon-more {
display: inline-block;
width: 44px;
height: 34px;
opacity: .5;
&:hover, &:focus {
opacity: 1;
}
}
.icon-boosted {
opacity: 1;
}
}
span {
/* opacity: 0.5; */
}
.entry-content {
display: flex;
}
.post-content {
flex-grow: 1;
}
.post-timestamp {
opacity: .7;
}
</style>
<style>
.post-message a {
text-decoration: underline;
.boost {
opacity: .5;
}
</style>

Wyświetl plik

@ -54,11 +54,21 @@ const mutations = {
setAccount(state, account) {
state.account = account
},
boostPost(state, post) {
Vue.set(state.timeline[post.id].action.values, 'boosted', true)
boostPost(state, { post, parentAnnounce }) {
if (typeof state.timeline[post.id] !== 'undefined') {
Vue.set(state.timeline[post.id].action.values, 'boosted', true)
}
if (typeof parentAnnounce.id !== 'undefined') {
Vue.set(state.timeline[parentAnnounce.id].cache[parentAnnounce.object].object.action.values, 'boosted', true)
}
},
unboostPost(state, post) {
Vue.set(state.timeline[post.id].action.values, 'boosted', false)
unboostPost(state, { post, parentAnnounce }) {
if (typeof state.timeline[post.id] !== 'undefined') {
Vue.set(state.timeline[post.id].action.values, 'boosted', false)
}
if (typeof parentAnnounce.id !== 'undefined') {
Vue.set(state.timeline[parentAnnounce.id].cache[parentAnnounce.object].object.action.values, 'boosted', false)
}
}
}
const getters = {
@ -103,10 +113,10 @@ const actions = {
console.error('Failed to delete the post', error)
})
},
postBoost(context, post) {
postBoost(context, { post, parentAnnounce }) {
return new Promise((resolve, reject) => {
axios.post(OC.generateUrl(`apps/social/api/v1/post/boost?postId=${post.id}`)).then((response) => {
context.commit('boostPost', post)
context.commit('boostPost', { post, parentAnnounce })
// eslint-disable-next-line no-console
console.log('Post boosted with token ' + response.data.result.token)
resolve(response)
@ -117,9 +127,9 @@ const actions = {
})
})
},
postUnBoost(context, post) {
postUnBoost(context, { post, parentAnnounce }) {
return axios.delete(OC.generateUrl(`apps/social/api/v1/post/boost?postId=${post.id}`)).then((response) => {
context.commit('unboostPost', post)
context.commit('unboostPost', { post, parentAnnounce })
// eslint-disable-next-line no-console
console.log('Boost deleted with token ' + response.data.result.token)
}).catch((error) => {