Merge remote-tracking branch 'origin/more-for-local-accounts' into more-for-local-accounts

Signed-off-by: Maxence Lange <maxence@artificial-owl.com>
pull/82/head
Maxence Lange 2018-11-30 10:02:25 -01:00
commit 75447af767
14 zmienionych plików z 185 dodań i 52 usunięć

Wyświetl plik

@ -5,7 +5,7 @@
Mockup: Mockup:
![](img/screenshot.png) ![](img/screenshot.png)
- **🙋 Find your friends:** No matter if they use Nextcloud, [Mastodon](https://en.wikipedia.org/wiki/Mastodon_(software)), [Friendica](https://en.wikipedia.org/wiki/Friendica), [GNU social](https://en.wikipedia.org/wiki/GNU_social) or others – you can follow them! - **🙋 Find your friends:** No matter if they use Nextcloud, [🐘 Mastodon](https://joinmastodon.org), [🇫 Friendica](https://friendi.ca), and soon [✱ diaspora*](https://joindiaspora.com), [👹 MediaGoblin](https://www.mediagoblin.org) and more – you can follow them!
- **📜 Profile info:** No need to fill out more profiles – your info from Nextcloud will be used and extended. - **📜 Profile info:** No need to fill out more profiles – your info from Nextcloud will be used and extended.
- **👐 Own your posts:** Everything you post stays on your Nextcloud! - **👐 Own your posts:** Everything you post stays on your Nextcloud!
- **🕸 Open standards:** We use the [ActivityPub](https://en.wikipedia.org/wiki/ActivityPub) standard! - **🕸 Open standards:** We use the [ActivityPub](https://en.wikipedia.org/wiki/ActivityPub) standard!
@ -13,8 +13,8 @@ Mockup:
## Development setup ## Development setup
1. ☁ Clone this into your `apps` folder of your Nextcloud 1. ☁ Clone this into the `apps` folder of your Nextcloud: `git clone https://github.com/nextcloud-gmbh/social.git`
2. 👩‍💻 In a terminal, run the command `make dev-setup` to install the dependencies 2. 👩‍💻 Run `make dev-setup` to install the dependencies
3. 🏗 Then to build the Javascript whenever you make changes, run `make build-js` 3. 🏗 Then to build the Javascript whenever you make changes, run `make build-js`
4. ✅ Enable the app through the app management of your Nextcloud 4. ✅ Enable the app through the app management of your Nextcloud
5. 🎉 Partytime! 5. 🎉 Partytime!

Wyświetl plik

@ -45,6 +45,7 @@ use OCA\Social\Service\ActorService;
use OCA\Social\Service\ConfigService; use OCA\Social\Service\ConfigService;
use OCA\Social\Service\MiscService; use OCA\Social\Service\MiscService;
use OCP\AppFramework\Controller; use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\ContentSecurityPolicy;
use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\Http\FileDisplayResponse; use OCP\AppFramework\Http\FileDisplayResponse;
use OCP\AppFramework\Http\RedirectResponse; use OCP\AppFramework\Http\RedirectResponse;
@ -186,7 +187,11 @@ class NavigationController extends Controller {
// neither. // neither.
} }
return new TemplateResponse(Application::APP_NAME, 'main', $data); $csp = new ContentSecurityPolicy();
$csp->addAllowedImageDomain('*');
$response = new TemplateResponse(Application::APP_NAME, 'main', $data);
$response->setContentSecurityPolicy($csp);
return $response;
} }

Wyświetl plik

@ -201,6 +201,32 @@ class CacheActorsRequest extends CacheActorsRequestBuilder {
} }
/**
* get Cached version of a local Actor, based on the preferred username
*
* @param string $account
*
* @return Person
* @throws CacheActorDoesNotExistException
*/
public function getFromLocalAccount(string $account): Person {
$qb = $this->getCacheActorsSelectSql();
$this->limitToPreferredUsername($qb, $account);
$this->limitToLocal($qb, true);
$this->leftJoinCacheDocuments($qb, 'icon_id');
$cursor = $qb->execute();
$data = $cursor->fetch();
$cursor->closeCursor();
if ($data === false) {
throw new CacheActorDoesNotExistException();
}
return $this->parseCacheActorsSelectSql($data);
}
/** /**
* @param string $search * @param string $search
* @param string $viewerId * @param string $viewerId

Wyświetl plik

@ -108,6 +108,22 @@ class FollowsRequestBuilder extends CoreRequestBuilder {
} }
/**
* Base of the Sql Select request for Shares
*
* @return IQueryBuilder
*/
protected function countFollowsSelectSql(): IQueryBuilder {
$qb = $this->dbConnection->getQueryBuilder();
$qb->selectAlias($qb->createFunction('COUNT(*)'), 'count')
->from(self::TABLE_SERVER_FOLLOWS, 'f');
$this->defaultSelectAlias = 'f';
return $qb;
}
/** /**
* Base of the Sql Delete request * Base of the Sql Delete request
* *

12
package-lock.json wygenerowano
Wyświetl plik

@ -7631,9 +7631,9 @@
} }
}, },
"nextcloud-vue": { "nextcloud-vue": {
"version": "0.4.2", "version": "0.4.6",
"resolved": "https://registry.npmjs.org/nextcloud-vue/-/nextcloud-vue-0.4.2.tgz", "resolved": "https://registry.npmjs.org/nextcloud-vue/-/nextcloud-vue-0.4.6.tgz",
"integrity": "sha512-4aePhl0VqpJw9LqNsQenPFmQ6I715vbOAlfVjPMdqVqoKnN9r2gr87/PfNeOLkVFrfo0vaYPx4QS0JSiLsAiqg==", "integrity": "sha512-INrIz3RmxxUCrM/xy2ytLvrrZr131p0DOT87A+IH0/+LFlfK//eR0uB32lSUsqh9Tb+bkTyu8Ztq9iuTrFfl2Q==",
"requires": { "requires": {
"@babel/polyfill": "^7.0.0", "@babel/polyfill": "^7.0.0",
"md5": "^2.2.1", "md5": "^2.2.1",
@ -8239,9 +8239,9 @@
"dev": true "dev": true
}, },
"popper.js": { "popper.js": {
"version": "1.14.5", "version": "1.14.6",
"resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.14.5.tgz", "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.14.6.tgz",
"integrity": "sha512-fs4Sd8bZLgEzrk8aS7Em1qh+wcawtE87kRUJQhK6+LndyV1HerX7+LURzAylVaTyWIn5NTB/lyjnWqw/AZ6Yrw==" "integrity": "sha512-AGwHGQBKumlk/MDfrSOf0JHhJCImdDMcGNoqKmKkU+68GFazv3CQ6q9r7Ja1sKDZmYWTckY/uLyEznheTDycnA=="
}, },
"posix-character-classes": { "posix-character-classes": {
"version": "0.1.1", "version": "0.1.1",

Wyświetl plik

@ -30,10 +30,11 @@
"@babel/polyfill": "^7.0.0", "@babel/polyfill": "^7.0.0",
"linkifyjs": "^2.1.7", "linkifyjs": "^2.1.7",
"nextcloud-axios": "^0.1.2", "nextcloud-axios": "^0.1.2",
"nextcloud-vue": "^0.4.2", "nextcloud-vue": "^0.4.6",
"tributejs": "^3.3.5", "tributejs": "^3.3.5",
"twemoji": "^11.2.0", "twemoji": "^11.2.0",
"uuid": "^3.3.2", "uuid": "^3.3.2",
"v-tooltip": "^2.0.0-rc.33",
"vue": "^2.5.16", "vue": "^2.5.16",
"vue-click-outside": "^1.0.7", "vue-click-outside": "^1.0.7",
"vue-contenteditable-directive": "^1.2.0", "vue-contenteditable-directive": "^1.2.0",

Wyświetl plik

@ -63,6 +63,7 @@ import axios from 'nextcloud-axios'
import TimelineEntry from './components/TimelineEntry' import TimelineEntry from './components/TimelineEntry'
import ProfileInfo from './components/ProfileInfo' import ProfileInfo from './components/ProfileInfo'
import Search from './components/Search' import Search from './components/Search'
import currentuserMixin from './mixins/currentUserMixin'
export default { export default {
name: 'App', name: 'App',
@ -75,6 +76,7 @@ export default {
ProfileInfo, ProfileInfo,
Search Search
}, },
mixins: [currentuserMixin],
data: function() { data: function() {
return { return {
infoHidden: false, infoHidden: false,
@ -84,21 +86,9 @@ export default {
} }
}, },
computed: { computed: {
url: function() {
return OC.linkTo('social', 'img/nextcloud.png')
},
currentUser: function() {
return OC.getCurrentUser()
},
socialId: function() {
return '@' + OC.getCurrentUser().uid + '@' + OC.getHost()
},
timeline: function() { timeline: function() {
return this.$store.getters.getTimeline return this.$store.getters.getTimeline
}, },
serverData: function() {
return this.$store.getters.getServerData
},
menu: function() { menu: function() {
let defaultCategories = [ let defaultCategories = [
{ {

Wyświetl plik

@ -28,7 +28,6 @@
<form class="new-post-form" @submit.prevent="createPost"> <form class="new-post-form" @submit.prevent="createPost">
<div class="author currentUser"> <div class="author currentUser">
{{ currentUser.displayName }} {{ currentUser.displayName }}
<span class="social-id">{{ socialId }}</span> <span class="social-id">{{ socialId }}</span>
</div> </div>
<vue-tribute :options="tributeOptions"> <vue-tribute :options="tributeOptions">
@ -385,29 +384,42 @@ export default {
} }
} }
}, },
activeState() {
return (type) => {
if (type === this.type) {
return true
} else {
return false
}
}
},
visibilityPopover() { visibilityPopover() {
return [ return [
{ {
action: () => { this.switchType('direct') }, action: () => { this.switchType('direct') },
icon: this.visibilityIconClass('direct'), icon: this.visibilityIconClass('direct'),
active: this.activeState('direct'),
text: t('social', 'Direct'), text: t('social', 'Direct'),
longtext: t('social', 'Post to mentioned users only') longtext: t('social', 'Post to mentioned users only')
}, },
{ {
action: () => { this.switchType('unlisted') }, action: () => { this.switchType('unlisted') },
icon: this.visibilityIconClass('unlisted'), icon: this.visibilityIconClass('unlisted'),
active: this.activeState('unlisted'),
text: t('social', 'Unlisted'), text: t('social', 'Unlisted'),
longtext: t('social', 'Do not post to public timelines') longtext: t('social', 'Do not post to public timelines')
}, },
{ {
action: () => { this.switchType('followers') }, action: () => { this.switchType('followers') },
icon: this.visibilityIconClass('followers'), icon: this.visibilityIconClass('followers'),
active: this.activeState('followers'),
text: t('social', 'Followers'), text: t('social', 'Followers'),
longtext: t('social', 'Post to followers only') longtext: t('social', 'Post to followers only')
}, },
{ {
action: () => { this.switchType('public') }, action: () => { this.switchType('public') },
icon: this.visibilityIconClass('public'), icon: this.visibilityIconClass('public'),
active: this.activeState('public'),
text: t('social', 'Public'), text: t('social', 'Public'),
longtext: t('social', 'Post to public timelines') longtext: t('social', 'Post to public timelines')
} }
@ -434,7 +446,7 @@ export default {
emoji.replaceWith(em) emoji.replaceWith(em)
}) })
let to = [] let to = []
const re = /@((\w+)(@[\w.]+)?)/g const re = /@(([\w-_.]+)(@[\w-.]+)?)/g
let match = null let match = null
do { do {
match = re.exec(element.innerText) match = re.exec(element.innerText)

Wyświetl plik

@ -81,12 +81,14 @@
<script> <script>
import { Avatar } from 'nextcloud-vue' import { Avatar } from 'nextcloud-vue'
import serverData from '../mixins/serverData'
export default { export default {
name: 'ProfileInfo', name: 'ProfileInfo',
components: { components: {
Avatar Avatar
}, },
mixins: [serverData],
props: { props: {
uid: { uid: {
type: String, type: String,
@ -98,17 +100,12 @@ export default {
if (typeof this.accountInfo.displayname !== 'undefined') { return this.accountInfo.displayname.value || '' } if (typeof this.accountInfo.displayname !== 'undefined') { return this.accountInfo.displayname.value || '' }
return this.uid return this.uid
}, },
serverData: function() {
return this.$store.getters.getServerData
},
accountInfo: function() { accountInfo: function() {
return this.$store.getters.getAccount(this.uid) return this.$store.getters.getAccount(this.uid)
} }
}, },
methods: { methods: {
follow() {
// TODO: implement following users
}
} }
} }

Wyświetl plik

@ -22,15 +22,13 @@
<template> <template>
<div class="social__wrapper"> <div class="social__wrapper">
<div v-if="results.length < 1" id="emptycontent" :class="{'icon-loading': loading}" <div v-if="results.length < 1" id="emptycontent" :class="{'icon-loading': loading}">
class=""> <div v-if="!loading" class="icon-search" />
<div class="icon-search" /> <h2 v-if="!loading">{{ t('social', 'No accounts found') }}</h2>
<h2>{{ t('social', 'No accounts found') }}</h2> <p v-if="!loading">No accounts found for {{ term }}</p>
<p>No accounts found for {{ term }}</p>
</div> </div>
<div v-if="match || results.length > 0"> <div v-if="match || results.length > 0">
<h3>{{ t('social', 'Search') }} {{ term }}</h3> <h3>{{ t('social', 'Search') }} {{ term }}</h3>
<UserEntry :item="match" />
<UserEntry v-for="result in results" :key="result.id" :item="result" /> <UserEntry v-for="result in results" :key="result.id" :item="result" />
</div> </div>
</div> </div>
@ -65,20 +63,32 @@ export default {
}, },
watch: { watch: {
term(val) { term(val) {
// TODO: debounce
this.search(val)
}
},
beforeMount() {
this.search(this.term)
},
methods: {
search(val) {
this.loading = true this.loading = true
this.accountSearch(val).then((response) => {
this.results = response.data.result.accounts
this.loading = false
})
const re = /@((\w+)(@[\w.]+)?)/g const re = /@((\w+)(@[\w.]+)?)/g
if (val.match(re)) { if (val.match(re)) {
this.remoteSearch(val).then((response) => { this.remoteSearch(val).then((response) => {
this.match = response.data.result.account this.match = response.data.result.account
this.accountSearch(val).then((response) => {
this.results = response.data.result.accounts
this.loading = false
})
}).catch((e) => { this.match = null }) }).catch((e) => { this.match = null })
} else {
this.accountSearch(val).then((response) => {
this.results = response.data.result.accounts
this.loading = false
})
} }
} },
},
methods: {
accountSearch(term) { accountSearch(term) {
this.loading = true this.loading = true
return axios.get(OC.generateUrl('apps/social/api/v1/accounts/search?search=' + term)) return axios.get(OC.generateUrl('apps/social/api/v1/accounts/search?search=' + term))

Wyświetl plik

@ -21,40 +21,47 @@
--> -->
<template> <template>
<div class="user-entry"> <div v-if="item" class="user-entry">
<div class="entry-content"> <div class="entry-content">
<div class="user-avatar"> <div class="user-avatar">
<avatar v-if="item.local" :size="32" :user="item.preferredUsername" /> <avatar v-if="item.local" :size="32" :user="item.preferredUsername" />
<avatar v-else url="" /> <avatar v-else :url="item.icon.url" />
</div> </div>
<div class="user-details"> <div class="user-details">
<router-link v-if="item.local" :to="{ name: 'profile', params: { account: item.account }}"> <router-link v-if="item.local" :to="{ name: 'profile', params: { account: item.account }}">
<span class="post-author">{{ item.preferredUsername }}</span> <span class="post-author">{{ item.preferredUsername }}</span>
</router-link> </router-link>
<a v-else :href="item.id" target="_blank" <a v-else :href="item.id" target="_blank"
rel="noreferrer">{{ item.preferredUsername }}</a> rel="noreferrer">{{ item.name }} <span class="user-description">{{ item.account }}</span></a>
<p class="user-description">{{ item.account }}</p> <!-- TODO check where the html is coming from to avoid security issues -->
<p v-html="item.summary" />
</div> </div>
<button v-if="item.following" class="icon-checkmark-color">Following</button> <button class="icon-checkmark-color" @click="unfollow()"
<button v-else class="primary">Follow</button> @mouseover="followingText=t('social', 'Unfollow')"
@mouseleave="followingText=t('social', 'Following')">{{ followingText }}</button>
<button class="primary" @click="follow">Follow</button>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import { Avatar } from 'nextcloud-vue' import { Avatar } from 'nextcloud-vue'
import follow from '../mixins/follow'
export default { export default {
name: 'UserEntry', name: 'UserEntry',
components: { components: {
Avatar Avatar
}, },
mixins: [
follow
],
props: { props: {
item: { type: Object, default: () => {} } item: { type: Object, default: () => {} }
}, },
data: function() { data: function() {
return { return {
followingText: t('social', 'Following')
} }
} }
} }

Wyświetl plik

@ -20,13 +20,19 @@
* *
*/ */
import serverData from './serverData'
export default { export default {
mixins: [
serverData
],
computed: { computed: {
currentUser: function() { currentUser: function() {
return OC.getCurrentUser() return OC.getCurrentUser()
}, },
socialId: function() { socialId: function() {
return '@' + OC.getCurrentUser().uid + '@' + OC.getHost() const url = document.createElement('a')
url.setAttribute('href', this.serverData.cloudAddress)
return '@' + OC.getCurrentUser().uid + '@' + url.hostname
} }
} }
} }

Wyświetl plik

@ -0,0 +1,34 @@
/*
* @copyright Copyright (c) 2018 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
*
* @license GNU AGPL version 3 or any later version
*
* 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 axios from 'nextcloud-axios'
export default {
methods: {
follow() {
return axios.put(OC.generateUrl('/apps/social/api/v1/account/follow?account=' + this.item.account))
},
unfollow() {
return axios.delete(OC.generateUrl('/apps/social/api/v1/account/follow?account=' + this.item.account))
}
}
}

Wyświetl plik

@ -0,0 +1,29 @@
/*
* @copyright Copyright (c) 2018 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
*
* @license GNU AGPL version 3 or any later version
*
* 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/>.
*
*/
export default {
computed: {
serverData: function() {
return this.$store.getters.getServerData
}
}
}