diff options
-rw-r--r-- | README.md | 6 | ||||
-rw-r--r-- | lib/Controller/NavigationController.php | 7 | ||||
-rw-r--r-- | lib/Db/CacheActorsRequest.php | 26 | ||||
-rw-r--r-- | lib/Db/FollowsRequestBuilder.php | 16 | ||||
-rw-r--r-- | package-lock.json | 12 | ||||
-rw-r--r-- | package.json | 3 | ||||
-rw-r--r-- | src/App.vue | 14 | ||||
-rw-r--r-- | src/components/Composer.vue | 16 | ||||
-rw-r--r-- | src/components/ProfileInfo.vue | 9 | ||||
-rw-r--r-- | src/components/Search.vue | 36 | ||||
-rw-r--r-- | src/components/UserEntry.vue | 21 | ||||
-rw-r--r-- | src/mixins/currentUserMixin.js | 8 | ||||
-rw-r--r-- | src/mixins/follow.js | 34 | ||||
-rw-r--r-- | src/mixins/serverData.js | 29 |
14 files changed, 185 insertions, 52 deletions
@@ -5,7 +5,7 @@ Mockup: ![](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. - **๐ Own your posts:** Everything you post stays on your Nextcloud! - **๐ธ Open standards:** We use the [ActivityPub](https://en.wikipedia.org/wiki/ActivityPub) standard! @@ -13,8 +13,8 @@ Mockup: ## Development setup -1. โ Clone this into your `apps` folder of your Nextcloud -2. ๐ฉโ๐ป In a terminal, run the command `make dev-setup` to install the dependencies +1. โ Clone this into the `apps` folder of your Nextcloud: `git clone https://github.com/nextcloud-gmbh/social.git` +2. ๐ฉโ๐ป Run `make dev-setup` to install the dependencies 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 5. ๐ Partytime! diff --git a/lib/Controller/NavigationController.php b/lib/Controller/NavigationController.php index 82713dbd..b14f46bd 100644 --- a/lib/Controller/NavigationController.php +++ b/lib/Controller/NavigationController.php @@ -45,6 +45,7 @@ use OCA\Social\Service\ActorService; use OCA\Social\Service\ConfigService; use OCA\Social\Service\MiscService; use OCP\AppFramework\Controller; +use OCP\AppFramework\Http\ContentSecurityPolicy; use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\Http\FileDisplayResponse; use OCP\AppFramework\Http\RedirectResponse; @@ -186,7 +187,11 @@ class NavigationController extends Controller { // 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; } diff --git a/lib/Db/CacheActorsRequest.php b/lib/Db/CacheActorsRequest.php index 85c4a2e8..1be44384 100644 --- a/lib/Db/CacheActorsRequest.php +++ b/lib/Db/CacheActorsRequest.php @@ -202,6 +202,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 $viewerId * diff --git a/lib/Db/FollowsRequestBuilder.php b/lib/Db/FollowsRequestBuilder.php index 0f50b37e..3a1d9445 100644 --- a/lib/Db/FollowsRequestBuilder.php +++ b/lib/Db/FollowsRequestBuilder.php @@ -109,6 +109,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 * * @return IQueryBuilder diff --git a/package-lock.json b/package-lock.json index 7ea690f1..0669807a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7631,9 +7631,9 @@ } }, "nextcloud-vue": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/nextcloud-vue/-/nextcloud-vue-0.4.2.tgz", - "integrity": "sha512-4aePhl0VqpJw9LqNsQenPFmQ6I715vbOAlfVjPMdqVqoKnN9r2gr87/PfNeOLkVFrfo0vaYPx4QS0JSiLsAiqg==", + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/nextcloud-vue/-/nextcloud-vue-0.4.6.tgz", + "integrity": "sha512-INrIz3RmxxUCrM/xy2ytLvrrZr131p0DOT87A+IH0/+LFlfK//eR0uB32lSUsqh9Tb+bkTyu8Ztq9iuTrFfl2Q==", "requires": { "@babel/polyfill": "^7.0.0", "md5": "^2.2.1", @@ -8239,9 +8239,9 @@ "dev": true }, "popper.js": { - "version": "1.14.5", - "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.14.5.tgz", - "integrity": "sha512-fs4Sd8bZLgEzrk8aS7Em1qh+wcawtE87kRUJQhK6+LndyV1HerX7+LURzAylVaTyWIn5NTB/lyjnWqw/AZ6Yrw==" + "version": "1.14.6", + "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.14.6.tgz", + "integrity": "sha512-AGwHGQBKumlk/MDfrSOf0JHhJCImdDMcGNoqKmKkU+68GFazv3CQ6q9r7Ja1sKDZmYWTckY/uLyEznheTDycnA==" }, "posix-character-classes": { "version": "0.1.1", diff --git a/package.json b/package.json index c8786392..667b9fc7 100644 --- a/package.json +++ b/package.json @@ -30,10 +30,11 @@ "@babel/polyfill": "^7.0.0", "linkifyjs": "^2.1.7", "nextcloud-axios": "^0.1.2", - "nextcloud-vue": "^0.4.2", + "nextcloud-vue": "^0.4.6", "tributejs": "^3.3.5", "twemoji": "^11.2.0", "uuid": "^3.3.2", + "v-tooltip": "^2.0.0-rc.33", "vue": "^2.5.16", "vue-click-outside": "^1.0.7", "vue-contenteditable-directive": "^1.2.0", diff --git a/src/App.vue b/src/App.vue index d27f5c4d..8d1f4c34 100644 --- a/src/App.vue +++ b/src/App.vue @@ -63,6 +63,7 @@ import axios from 'nextcloud-axios' import TimelineEntry from './components/TimelineEntry' import ProfileInfo from './components/ProfileInfo' import Search from './components/Search' +import currentuserMixin from './mixins/currentUserMixin' export default { name: 'App', @@ -75,6 +76,7 @@ export default { ProfileInfo, Search }, + mixins: [currentuserMixin], data: function() { return { infoHidden: false, @@ -84,21 +86,9 @@ export default { } }, 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() { return this.$store.getters.getTimeline }, - serverData: function() { - return this.$store.getters.getServerData - }, menu: function() { let defaultCategories = [ { diff --git a/src/components/Composer.vue b/src/components/Composer.vue index f3fb5bbc..66794749 100644 --- a/src/components/Composer.vue +++ b/src/components/Composer.vue @@ -28,7 +28,6 @@ <form class="new-post-form" @submit.prevent="createPost"> <div class="author currentUser"> {{ currentUser.displayName }} - <span class="social-id">{{ socialId }}</span> </div> <vue-tribute :options="tributeOptions"> @@ -385,29 +384,42 @@ export default { } } }, + activeState() { + return (type) => { + if (type === this.type) { + return true + } else { + return false + } + } + }, visibilityPopover() { return [ { action: () => { this.switchType('direct') }, icon: this.visibilityIconClass('direct'), + active: this.activeState('direct'), text: t('social', 'Direct'), longtext: t('social', 'Post to mentioned users only') }, { action: () => { this.switchType('unlisted') }, icon: this.visibilityIconClass('unlisted'), + active: this.activeState('unlisted'), text: t('social', 'Unlisted'), longtext: t('social', 'Do not post to public timelines') }, { action: () => { this.switchType('followers') }, icon: this.visibilityIconClass('followers'), + active: this.activeState('followers'), text: t('social', 'Followers'), longtext: t('social', 'Post to followers only') }, { action: () => { this.switchType('public') }, icon: this.visibilityIconClass('public'), + active: this.activeState('public'), text: t('social', 'Public'), longtext: t('social', 'Post to public timelines') } @@ -434,7 +446,7 @@ export default { emoji.replaceWith(em) }) let to = [] - const re = /@((\w+)(@[\w.]+)?)/g + const re = /@(([\w-_.]+)(@[\w-.]+)?)/g let match = null do { match = re.exec(element.innerText) diff --git a/src/components/ProfileInfo.vue b/src/components/ProfileInfo.vue index d1f2bd0c..6506e125 100644 --- a/src/components/ProfileInfo.vue +++ b/src/components/ProfileInfo.vue @@ -81,12 +81,14 @@ <script> import { Avatar } from 'nextcloud-vue' +import serverData from '../mixins/serverData' export default { name: 'ProfileInfo', components: { Avatar }, + mixins: [serverData], props: { uid: { type: String, @@ -98,17 +100,12 @@ export default { if (typeof this.accountInfo.displayname !== 'undefined') { return this.accountInfo.displayname.value || '' } return this.uid }, - serverData: function() { - return this.$store.getters.getServerData - }, accountInfo: function() { return this.$store.getters.getAccount(this.uid) } }, methods: { - follow() { - // TODO: implement following users - } + } } diff --git a/src/components/Search.vue b/src/components/Search.vue index 0cecb528..be9165ff 100644 --- a/src/components/Search.vue +++ b/src/components/Search.vue @@ -22,15 +22,13 @@ <template> <div class="social__wrapper"> - <div v-if="results.length < 1" id="emptycontent" :class="{'icon-loading': loading}" - class=""> - <div class="icon-search" /> - <h2>{{ t('social', 'No accounts found') }}</h2> - <p>No accounts found for {{ term }}</p> + <div v-if="results.length < 1" id="emptycontent" :class="{'icon-loading': loading}"> + <div v-if="!loading" class="icon-search" /> + <h2 v-if="!loading">{{ t('social', 'No accounts found') }}</h2> + <p v-if="!loading">No accounts found for {{ term }}</p> </div> <div v-if="match || results.length > 0"> <h3>{{ t('social', 'Search') }} {{ term }}</h3> - <UserEntry :item="match" /> <UserEntry v-for="result in results" :key="result.id" :item="result" /> </div> </div> @@ -65,20 +63,32 @@ export default { }, watch: { term(val) { + // TODO: debounce + this.search(val) + } + }, + beforeMount() { + this.search(this.term) + }, + methods: { + search(val) { this.loading = true - this.accountSearch(val).then((response) => { - this.results = response.data.result.accounts - this.loading = false - }) const re = /@((\w+)(@[\w.]+)?)/g if (val.match(re)) { this.remoteSearch(val).then((response) => { 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 }) + } else { + this.accountSearch(val).then((response) => { + this.results = response.data.result.accounts + this.loading = false + }) } - } - }, - methods: { + }, accountSearch(term) { this.loading = true return axios.get(OC.generateUrl('apps/social/api/v1/accounts/search?search=' + term)) diff --git a/src/components/UserEntry.vue b/src/components/UserEntry.vue index b615d23c..ecf6f3a7 100644 --- a/src/components/UserEntry.vue +++ b/src/components/UserEntry.vue @@ -21,40 +21,47 @@ --> <template> - <div class="user-entry"> + <div v-if="item" class="user-entry"> <div class="entry-content"> <div class="user-avatar"> <avatar v-if="item.local" :size="32" :user="item.preferredUsername" /> - <avatar v-else url="" /> + <avatar v-else :url="item.icon.url" /> </div> <div class="user-details"> <router-link v-if="item.local" :to="{ name: 'profile', params: { account: item.account }}"> <span class="post-author">{{ item.preferredUsername }}</span> </router-link> <a v-else :href="item.id" target="_blank" - rel="noreferrer">{{ item.preferredUsername }}</a> - <p class="user-description">{{ item.account }}</p> + rel="noreferrer">{{ item.name }} <span class="user-description">{{ item.account }}</span></a> + <!-- TODO check where the html is coming from to avoid security issues --> + <p v-html="item.summary" /> </div> - <button v-if="item.following" class="icon-checkmark-color">Following</button> - <button v-else class="primary">Follow</button> + <button class="icon-checkmark-color" @click="unfollow()" + @mouseover="followingText=t('social', 'Unfollow')" + @mouseleave="followingText=t('social', 'Following')">{{ followingText }}</button> + <button class="primary" @click="follow">Follow</button> </div> </div> </template> <script> import { Avatar } from 'nextcloud-vue' +import follow from '../mixins/follow' export default { name: 'UserEntry', components: { Avatar }, + mixins: [ + follow + ], props: { item: { type: Object, default: () => {} } }, data: function() { return { - + followingText: t('social', 'Following') } } } diff --git a/src/mixins/currentUserMixin.js b/src/mixins/currentUserMixin.js index 434aeac5..8bc5adfa 100644 --- a/src/mixins/currentUserMixin.js +++ b/src/mixins/currentUserMixin.js @@ -20,13 +20,19 @@ * */ +import serverData from './serverData' export default { + mixins: [ + serverData + ], computed: { currentUser: function() { return OC.getCurrentUser() }, 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 } } } diff --git a/src/mixins/follow.js b/src/mixins/follow.js new file mode 100644 index 00000000..09fde974 --- /dev/null +++ b/src/mixins/follow.js @@ -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)) + } + } +} diff --git a/src/mixins/serverData.js b/src/mixins/serverData.js new file mode 100644 index 00000000..24783e3c --- /dev/null +++ b/src/mixins/serverData.js @@ -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 + } + } +} |