Merge pull request from nextcloud-gmbh/frontend

Frontend implementation
pull/14/head
Maxence Lange 2018-10-26 10:34:56 +02:00 zatwierdzone przez GitHub
commit 17b1db4eb4
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
28 zmienionych plików z 15231 dodań i 4233 usunięć

14
.babelrc.js 100644
Wyświetl plik

@ -0,0 +1,14 @@
module.exports = {
plugins: ['@babel/plugin-syntax-dynamic-import'],
presets: [
[
'@babel/preset-env',
{
targets: {
browsers: ['last 2 versions', 'ie >= 11']
}
}
]
]
}

Wyświetl plik

@ -1,3 +1,67 @@
module.exports = {
"extends": "standard"
};
root: true,
env: {
browser: true,
es6: true,
node: true,
jest: true
},
globals: {
t: true,
n: true,
OC: true,
OCA: true,
Vue: true,
VueRouter: true
},
parserOptions: {
parser: 'babel-eslint',
ecmaVersion: 6
},
extends: [
'eslint:recommended',
'plugin:node/recommended',
'plugin:vue/essential',
'plugin:vue/recommended',
'standard'
],
plugins: ['vue', 'node'],
rules: {
// space before function ()
'space-before-function-paren': ['error', 'never'],
// curly braces always space
'object-curly-spacing': ['error', 'always'],
// stay consistent with array brackets
'array-bracket-newline': ['error', 'consistent'],
// 1tbs brace style
'brace-style': 'error',
// tabs only
indent: ['error', 'tab'],
'no-tabs': 0,
'vue/html-indent': ['error', 'tab'],
// only debug console
'no-console': ['error', { allow: ['error', 'warn', 'debug'] }],
// classes blocks
'padded-blocks': ['error', { classes: 'always' }],
// always have the operator in front
'operator-linebreak': ['error', 'before'],
// ternary on multiline
'multiline-ternary': ['error', 'always-multiline'],
// es6 import/export and require
'node/no-unpublished-require': ['off'],
'node/no-unsupported-features/es-syntax': ['off'],
// space before self-closing elements
'vue/html-closing-bracket-spacing': 'error',
// code spacing with attributes
'vue/max-attributes-per-line': [
'error',
{
singleline: 3,
multiline: {
max: 3,
allowFirstLine: true
}
}
]
}
}

4
.gitignore vendored
Wyświetl plik

@ -1,4 +1,4 @@
js/
\.idea/
node_modules/
vendor/

Wyświetl plik

@ -10,8 +10,14 @@
return [
'routes' => [
['name' => 'Navigation#navigate', 'url' => '/', 'verb' => 'GET'],
['name' => 'Navigation#timeline', 'url' => '/timeline/{path}', 'verb' => 'GET', 'requirements' => ['path' => '.+'], 'defaults' => ['path' => '']],
['name' => 'Navigation#account', 'url' => '/account/{path}', 'verb' => 'GET', 'requirements' => ['path' => '.+'], 'defaults' => ['path' => '']],
['name' => 'Navigation#public', 'url' => '/{username}', 'verb' => 'GET'],
// ['name' => 'Account#create', 'url' => '/local/account/{username}', 'verb' => 'POST'],
['name' => 'Account#info', 'url' => '/local/account/{username}', 'verb' => 'GET'],
['name' => 'ActivityPub#sharedInbox', 'url' => '/inbox', 'verb' => 'POST'],
['name' => 'ActivityPub#actor', 'url' => '/users/{username}', 'verb' => 'GET'],

BIN
img/nextcloud.png 100644

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 17 KiB

File diff suppressed because one or more lines are too long

Wyświetl plik

@ -35,9 +35,14 @@ use OCA\Social\AppInfo\Application;
use OCA\Social\Service\ActorService;
use OCA\Social\Service\ConfigService;
use OCA\Social\Service\MiscService;
use OCP\Accounts\IAccountManager;
use OCP\Accounts\IAccountProperty;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\Http\NotFoundResponse;
use OCP\AppFramework\Http\Response;
use OCP\IRequest;
use OCP\IUserManager;
class AccountController extends Controller {
@ -56,6 +61,9 @@ class AccountController extends Controller {
/** @var MiscService */
private $miscService;
/** @var IAccountManager */
private $accountManager;
/**
* AccountController constructor.
@ -67,8 +75,9 @@ class AccountController extends Controller {
* @param MiscService $miscService
*/
public function __construct(
IRequest $request, string $userId, ConfigService $configService,
ActorService $actorService, MiscService $miscService
IRequest $request, ConfigService $configService,
ActorService $actorService, MiscService $miscService,
IAccountManager $accountManager, IUserManager $userManager, string $userId = null
) {
parent::__construct(Application::APP_NAME, $request);
@ -76,6 +85,8 @@ class AccountController extends Controller {
$this->configService = $configService;
$this->actorService = $actorService;
$this->miscService = $miscService;
$this->accountManager = $accountManager;
$this->userManager = $userManager;
}
@ -83,6 +94,9 @@ class AccountController extends Controller {
* Called by the frontend to create a new Social account
*
* @NoAdminRequired
* @NoCSRFRequired
* @NoAdminRequired
* @NoSubAdminRequired
*
* @param string $username
*
@ -98,6 +112,45 @@ class AccountController extends Controller {
}
}
/**
* @PublicPage
* @NoAdminRequired
* @NoCSRFRequired
* @NoAdminRequired
* @NoSubAdminRequired
* @param string $username
* @return DataResponse
*/
public function info(string $username): Response {
$user = $this->userManager->get($username);
if ($user === null) {
// TODO: Proper handling of external accounts
$props = [];
$props['cloudId'] = $username;
$props['displayname'] = ['value' => 'External account'];
$props['posts'] = 1;
$props['following'] = 2;
$props['followers'] = 3;
return new DataResponse($props);
}
$account = $this->accountManager->getAccount($user);
/** @var IAccountProperty[] $props */
$props = $account->getFilteredProperties(IAccountManager::VISIBILITY_PUBLIC, null);
if ($this->userId !== null) {
$props = array_merge($props, $account->getFilteredProperties(IAccountManager::VISIBILITY_CONTACTS_ONLY, null));
}
if (\array_key_exists('avatar', $props)) {
$props['avatar']->setValue(\OC::$server->getURLGenerator()->linkToRouteAbsolute('core.avatar.getAvatar', ['userId' => $username, 'size' => 128]));
}
// Add counters
$props['cloudId'] = $user->getCloudId();
$props['posts'] = 1;
$props['following'] = 2;
$props['followers'] = 3;
return new DataResponse($props);
}
}

Wyświetl plik

@ -30,14 +30,18 @@ declare(strict_types=1);
namespace OCA\Social\Controller;
use OC\Accounts\AccountManager;
use OC\User\NoUserException;
use OCA\Social\AppInfo\Application;
use OCA\Social\Exceptions\AccountAlreadyExistsException;
use OCA\Social\Service\ActorService;
use OCA\Social\Service\MiscService;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\RedirectResponse;
use OCP\AppFramework\Http\Template\PublicTemplateResponse;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\IConfig;
use OCP\IL10N;
use OCP\IRequest;
use OCP\IURLGenerator;
@ -58,6 +62,8 @@ class NavigationController extends Controller {
/** @var MiscService */
private $miscService;
/** @var IL10N */
private $l10n;
/**
* NavigationController constructor.
@ -70,8 +76,8 @@ class NavigationController extends Controller {
* @param MiscService $miscService
*/
public function __construct(
IRequest $request, string $userId, IConfig $config, IURLGenerator $urlGenerator,
ActorService $actorService, MiscService $miscService
IRequest $request, $userId, IConfig $config, IURLGenerator $urlGenerator,
ActorService $actorService, MiscService $miscService, IL10N $l10n
) {
parent::__construct(Application::APP_NAME, $request);
@ -81,6 +87,7 @@ class NavigationController extends Controller {
$this->actorService = $actorService;
$this->miscService = $miscService;
$this->l10n = $l10n;
}
@ -94,8 +101,12 @@ class NavigationController extends Controller {
* @return TemplateResponse
* @throws NoUserException
*/
public function navigate(): TemplateResponse {
$data = [];
public function navigate($path = ''): TemplateResponse {
$data = [
'serverData' => [
'public' => false,
]
];
try {
$this->actorService->createActor($this->userId, $this->userId);
@ -103,8 +114,59 @@ class NavigationController extends Controller {
// we do nothing
}
return new TemplateResponse(Application::APP_NAME, 'main', $data);
}
/**
* Display the navigation page of the Social app.
*
* @NoCSRFRequired
* @NoAdminRequired
* @NoSubAdminRequired
*
* @return TemplateResponse
* @throws NoUserException
*/
public function timeline($path = ''): TemplateResponse {
return $this->navigate();
}
/**
* Display the navigation page of the Social app.
*
* @NoCSRFRequired
* @NoAdminRequired
* @NoSubAdminRequired
*
* @return TemplateResponse
* @throws NoUserException
*/
public function account($path = ''): TemplateResponse {
return $this->navigate();
}
/**
* @NoCSRFRequired
* @PublicPage
*
* @param $username
* @return RedirectResponse|PublicTemplateResponse
*/
public function public($username) {
if (\OC::$server->getUserSession()->isLoggedIn()) {
return $this->navigate();
}
$data = [
'serverData' => [
'public' => true,
]
];
$page = new PublicTemplateResponse(Application::APP_NAME, 'main', $data);
$page->setHeaderTitle($this->l10n->t('Social') . ' ' . $username);
return $page;
}
}

17436
package-lock.json wygenerowano

Plik diff jest za duży Load Diff

Wyświetl plik

@ -27,7 +27,9 @@
"test:coverage": "jest --coverage"
},
"dependencies": {
"vue-components": "github:nextcloud/vue-components#init",
"@babel/polyfill": "^7.0.0",
"nextcloud-axios": "^0.1.2",
"nextcloud-vue": "file:/home/jus/repos/nextcloud/vue-components",
"uuid": "^3.3.2",
"vue": "^2.5.16",
"vue-click-outside": "^1.0.7",
@ -43,17 +45,18 @@
"node": ">=10.0.0"
},
"devDependencies": {
"@vue/test-utils": "^1.0.0-beta.20",
"babel-core": "^6.26.3",
"@babel/core": "^7.1.2",
"@babel/plugin-syntax-dynamic-import": "^7.0.0",
"@babel/preset-env": "^7.1.0",
"@vue/test-utils": "^1.0.0-beta.25",
"babel-eslint": "^8.2.5",
"babel-jest": "^23.4.0",
"babel-loader": "^7.1.4",
"babel-preset-env": "^1.7.0",
"babel-jest": "^23.6.0",
"babel-loader": "^8.0.4",
"css-loader": "^0.28.11",
"eslint": "^4.19.1",
"eslint-config-standard": "^11.0.0",
"eslint-friendly-formatter": "^4.0.1",
"eslint-loader": "^2.0.0",
"eslint-loader": "^2.1.1",
"eslint-plugin-import": "^2.13.0",
"eslint-plugin-node": "^7.0.1",
"eslint-plugin-promise": "^3.8.0",
@ -61,24 +64,24 @@
"eslint-plugin-vue": "^4.5.0",
"extract-text-webpack-plugin": "^3.0.2",
"file-loader": "^1.1.11",
"jest": "^23.4.0",
"jest": "^23.6.0",
"jest-serializer-vue": "^2.0.2",
"mini-css-extract-plugin": "^0.4.1",
"node-sass": "^4.9.2",
"mini-css-extract-plugin": "^0.4.4",
"node-sass": "^4.9.4",
"prettier-eslint": "^8.8.2",
"raw-loader": "^0.5.1",
"sass-loader": "^7.0.3",
"sass-resources-loader": "^1.3.3",
"sass-resources-loader": "^1.3.4",
"stylelint": "^8.4.0",
"stylelint-config-recommended-scss": "^3.2.0",
"stylelint-scss": "^3.1.3",
"stylelint-scss": "^3.3.2",
"stylelint-webpack-plugin": "^0.10.5",
"vue-jest": "^2.6.0",
"vue-loader": "^15.2.4",
"vue-loader": "^15.4.2",
"vue-style-loader": "^4.1.1",
"vue-template-compiler": "^2.5.16",
"webpack": "^4.16.0",
"webpack-cli": "^3.0.4",
"webpack": "^4.23.1",
"webpack-cli": "^3.1.2",
"webpack-merge": "^4.1.2"
},
"jest": {

Wyświetl plik

@ -1,163 +1,150 @@
<template>
<div id="content" class="app-social">
<div id="app-navigation">
<app-navigation :menu="menu">
<!--<template slot="settings-content">Settings</template>-->
</app-navigation>
<div class="app-social">
<div v-if="!serverData.public" id="app-navigation">
<app-navigation :menu="menu" />
</div>
<div id="app-content">
<div class="social__container">
<h2>🎉 Nextcloud becomes part of the federated social networks!</h2>
</div>
<router-view :key="$route.fullPath" />
</div>
</div>
</template>
<script>
import {
PopoverMenu,
AppNavigation
} from 'vue-components';
<style scoped>
.app-social {
width: 100%;
}
</style>
export default {
name: 'App',
components: {
PopoverMenu, AppNavigation
<script>
import {
PopoverMenu,
AppNavigation,
Multiselect,
Avatar
} from 'nextcloud-vue'
import TimelineEntry from './components/TimelineEntry'
import ProfileInfo from './components/ProfileInfo'
export default {
name: 'App',
components: {
PopoverMenu,
AppNavigation,
TimelineEntry,
Multiselect,
Avatar,
ProfileInfo
},
data: function() {
return {
infoHidden: false,
state: []
}
},
computed: {
url: function() {
return OC.linkTo('social', 'img/nextcloud.png')
},
data: function () {
return {
isOpen: false,
// example popover in the content
menuPopover: [
{
icon: 'icon-delete',
text: 'Delete item',
action: () => {
alert('Deleted!');
}
},
{
icon: 'icon-user',
text: 'Nextcloud website',
action: () => {},
href: 'https://nextcloud.com'
},
{
icon: 'icon-details',
longtext: 'Add item',
action: () => {
alert('details');
}
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 = [
{
id: 'social-timeline',
classes: [],
icon: 'icon-category-monitoring',
text: t('social', 'Timeline'),
router: {
name: 'timeline'
}
]
};
},
computed: {
// App navigation
menu: function () {
let defaultCategories = [
{
id: 'social-timeline',
classes: [],
href: '#',
icon: 'icon-category-monitoring',
text: t('social', 'Timeline'),
},
{
id: 'social-account',
classes: [],
icon: 'icon-user',
text: t('social', 'Your account'),
router: {
name: 'profile',
params: { account: this.currentUser.uid }
}
},
{
id: 'social-friends',
classes: [],
href: '#',
icon: 'icon-category-social',
text: t('social', 'Friends')
},
{
id: 'social-favorites',
classes: [],
href: '#',
icon: 'icon-favorite',
text: t('social', 'Favorites')
},
{
id: 'social-direct-messages',
classes: [],
href: '#',
icon: 'icon-comment',
utils: {
counter: 3
},
{
id: 'social-your-posts',
classes: [],
href: '#',
icon: 'icon-user',
text: t('social', 'Your posts'),
},
{
id: 'social-friends',
classes: [],
href: '#',
icon: 'icon-category-social',
text: t('social', 'Friends'),
},
{
id: 'social-favorites',
classes: [],
href: '#',
icon: 'icon-favorite',
text: t('social', 'Favorites'),
},
{
id: 'social-direct-messages',
classes: [],
href: '#',
icon: 'icon-comment',
utils: {
counter: 3,
},
text: t('social', 'Direct messages'),
},
/*{
caption: true,
text: t('social', 'Popular topics'),
},
{
id: 'social-topic-nextcloud',
classes: [],
icon: 'icon-tag',
href: '#',
utils: {
actions: [
{
icon: 'icon-delete',
text: t('settings', 'Remove topic'),
action: function () {
console.log('remove')
}
}
]
},
text: t('settings', '#nextcloud'),
},
{
id: 'social-topic-mastodon',
classes: [],
icon: 'icon-tag',
href: '#',
utils: {
actions: [
{
icon: 'icon-delete',
text: t('settings', 'Remove topic'),
action: function () {
console.log('remove')
}
}
]
},
text: t('social', '#mastodon'),
},
{
id: 'social-topic-privacy',
classes: [],
icon: 'icon-tag',
href: '#',
utils: {
actions: [
{
icon: 'icon-delete',
text: t('settings', 'Remove topic'),
action: function () {
console.log('remove')
}
}
]
},
text: t('social', '#privacy'),
},*/
];
return {
items: defaultCategories,
loading: false
text: t('social', 'Direct messages')
}
]
return {
items: defaultCategories,
loading: false
}
}
},
beforeMount: function() {
// importing server data into the store
const serverDataElmt = document.getElementById('serverData')
if (serverDataElmt !== null) {
this.$store.commit('setServerData', JSON.parse(document.getElementById('serverData').dataset.server))
}
let example = {
message: 'Want to #DropDropbox? #DeleteGoogle? #decentralize? We got you covered, easy as a piece of 🥞\n'
+ '\n'
+ 'Get started right now: https://nextcloud.com/signup',
author: 'Nextcloud 📱☁️💻',
authorId: '@nextcloud@mastodon.xyz',
authorAvatar: OC.linkTo('social', 'img/nextcloud.png'),
timestamp: '1 day ago'
}
let data = []
for (let i = 0; i < 3; i++) {
example.id = Math.floor((Math.random() * 100))
data.push(example)
}
data.push({
message: 'Want to #DropDropbox? #DeleteGoogle? #decentralize? We got you covered, easy as a piece of 🥞\n'
+ '\n'
+ 'Get started right now: https://nextcloud.com/signup',
author: 'Admin☁💻',
authorId: 'admin',
authorAvatar: OC.linkTo('social', 'img/nextcloud.png'),
timestamp: '1 day ago'
})
this.$store.commit('addToTimeline', data)
},
methods: {
hideInfo() {
this.infoHidden = true
}
}
}
</script>

Wyświetl plik

@ -0,0 +1,115 @@
<!--
- @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/>.
-
-->
<template>
<div v-if="uid && accountInfo" class="user-profile">
<div class="user-profile--info">
<avatar :user="uid" :display-name="displayName" :size="128" />
<h2>{{ displayName }}</h2>
<p>{{ accountInfo.cloudId }}</p>
<p v-if="accountInfo.website">Website: <a :href="accountInfo.website.value">{{ accountInfo.website.value }}</a></p>
<button v-if="!serverData.public" class="primary" @click="follow">Follow this user</button>
</div>
<ul class="user-profile--sections">
<li>
<router-link to="./" class="icon-category-monitoring">{{ accountInfo.posts }} posts</router-link>
</li>
<li>
<router-link to="./following" class="icon-category-social">{{ accountInfo.following }} following</router-link>
</li>
<li>
<router-link to="./followers" class="icon-category-social">{{ accountInfo.followers }} followers</router-link>
</li>
</ul>
</div>
</template>
<style scoped>
.user-profile {
display: flex;
width: 100%;
text-align: center;
padding-top: 20px;
align-items: flex-end;
margin-bottom: 20px;
}
h2 {
margin-bottom: 5px;
}
.user-profile--info {
width: 40%;
}
.user-profile--sections {
width: 60%;
display: flex;
margin-bottom: 30px;
}
.user-profile--sections li {
flex-grow: 1;
}
.user-profile--sections li a {
padding-left: 24px;
background-position: 0 center;
height: 40px;
opacity: .6;
}
.user-profile--sections li a.active {
opacity: 1;
}
</style>
<script>
import { Avatar } from 'nextcloud-vue'
export default {
name: 'ProfileInfo',
components: {
Avatar
},
props: {
uid: {
type: String,
default: ''
}
},
computed: {
displayName() {
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
}
}
}
</script>

Wyświetl plik

@ -0,0 +1,89 @@
<template>
<div class="timeline-entry">
<div class="entry-content">
<div class="post-avatar">
<avatar :size="32" :url="item.authorAvatar" />
</div>
<div class="post-content">
<div class="post-author-wrapper">
<router-link :to="{ name: 'profile', params: { account: item.authorId }}">
<span class="post-author">{{ item.author }}</span>
<span class="post-author-id">{{ item.authorId }}</span>
</router-link>
</div>
<div class="post-message" v-html="formatedMessage" />
</div>
<div class="post-timestamp">{{ item.timestamp }}</div>
</div>
</div>
</template>
<script>
import { Avatar } from 'nextcloud-vue'
export default {
name: 'TimelineEntry',
components: {
Avatar
},
props: {
item: { type: Object, default: () => {} }
},
data: function() {
return {
}
},
computed: {
formatedMessage: function() {
let message = this.item.message
message = message.replace(/(?:\r\n|\r|\n)/g, '<br />')
return message
}
}
}
</script>
<style scoped>
.timeline-entry {
padding: 10px;
margin-bottom: 10px;
}
.social__welcome h3 {
margin-top: 0;
}
.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;
}
.timestamp {
float: right;
}
span {
/* opacity: 0.5; */
}
.entry-content {
display: flex;
}
.post-content {
flex-grow: 1;
}
.post-timestamp {
opacity: .7;
}
</style>

Wyświetl plik

@ -19,9 +19,25 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
import '@babel/polyfill'
import Vue from 'vue'
import { sync } from 'vuex-router-sync'
import App from './App'
import store from './store'
import router from './router'
sync(store, router)
// CSP config for webpack dynamic chunk loading
// eslint-disable-next-line
__webpack_nonce__ = btoa(OC.requestToken)
// Correct the root of the app for chunk loading
// OC.linkTo matches the apps folders
// eslint-disable-next-line
__webpack_public_path__ = OC.linkTo('social', 'js/')
Vue.prototype.t = t
Vue.prototype.n = n
@ -30,5 +46,7 @@ Vue.prototype.OCA = OCA
/* eslint-disable-next-line no-new */
new Vue({
render: h => h(App)
}).$mount('#content')
router: router,
render: h => h(App),
store: store
}).$mount('#vue-content')

87
src/router.js 100644
Wyświetl plik

@ -0,0 +1,87 @@
/**
* @copyright Copyright (c) 2018 Julius Härtl <jus@bitgrid.net>
* @copyright Copyright (c) 2018 John Molakvoæ <skjnldsv@protonmail.com>
*
* @author Julius Härtl <jus@bitgrid.net>
* @author John Molakvoæ <skjnldsv@protonmail.com>
*
* @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 Vue from 'vue'
import Router from 'vue-router'
// Dynamic loading
const Timeline = () => import('./views/Timeline')
const Profile = () => import('./views/Profile')
const ProfileTimeline = () => import('./views/ProfileTimeline')
const ProfileFollowers = () => import('./views/ProfileFollowers')
Vue.use(Router)
export default new Router({
mode: 'history',
// if index.php is in the url AND we got this far, then it's working:
// let's keep using index.php in the url
base: OC.generateUrl(''),
linkActiveClass: 'active',
routes: [
{
path: '/:index(index.php/)?apps/social/',
components: {
default: Timeline
},
props: true,
name: 'timeline'
},
{
path: '/:index(index.php/)?apps/social/account/:account',
components: {
default: Profile
},
props: true,
name: 'profile',
children: [
{
path: '',
components: {
details: ProfileTimeline
}
},
{
path: 'followers',
components: {
default: Profile,
details: ProfileFollowers
}
},
{
path: 'following',
components: {
default: Profile,
details: ProfileFollowers
}
}
]
},
{
path: '/:index(index.php/)?apps/social/:account',
component: Profile,
props: true,
name: 'public'
}
]
})

Wyświetl plik

@ -0,0 +1,47 @@
/*
* @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'
import Vue from 'vue'
const state = {
accounts: {}
}
const mutations = {
addAccount(state, { uid, data }) {
Vue.set(state.accounts, uid, data)
}
}
const getters = {
getAccount(state) {
return (uid) => state.accounts[uid]
}
}
const actions = {
fetchAccountInfo(context, uid) {
axios.get(OC.generateUrl('apps/social/local/account/' + uid)).then((response) => {
context.commit('addAccount', { uid: uid, data: response.data })
})
}
}
export default { state, mutations, getters, actions }

41
src/store/index.js 100644
Wyświetl plik

@ -0,0 +1,41 @@
/*
* @copyright Copyright (c) 2018 John Molakvoæ <skjnldsv@protonmail.com>
*
* @author John Molakvoæ <skjnldsv@protonmail.com>
* @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 Vue from 'vue'
import Vuex from 'vuex'
import timeline from './timeline'
import account from './account'
import settings from './settings'
Vue.use(Vuex)
const debug = process.env.NODE_ENV !== 'production'
export default new Vuex.Store({
modules: {
timeline,
account,
settings
},
strict: debug
})

Wyświetl plik

@ -0,0 +1,38 @@
/*
* @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/>.
*
*/
const state = {
serverData: {}
}
const mutations = {
setServerData(state, data) {
state.serverData = data
}
}
const getters = {
getServerData(state) {
return state.serverData
}
}
const actions = {}
export default { state, mutations, getters, actions }

Wyświetl plik

@ -0,0 +1,40 @@
/*
* @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/>.
*
*/
const state = {
timeline: []
}
const mutations = {
addToTimeline(state, data) {
for (let item in data) {
state.timeline.push(data[item])
}
}
}
const getters = {
getTimeline(state) {
return state.timeline
}
}
const actions = {}
export default { state, mutations, getters, actions }

Wyświetl plik

@ -0,0 +1,156 @@
<!--
- @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/>.
-
-->
<template>
<div class="social__wrapper">
<profile-info :uid="uid" />
<div class="social__container">
<router-view name="details" />
</div>
</div>
</template>
<style scoped>
.social__wrapper {
max-width: 700px;
margin: 15px auto;
}
.social__welcome {
max-width: 700px;
margin: 15px auto;
padding: 15px;
border-radius: 10px;
background-color: var(--color-background-dark);
}
.social__welcome h3 {
margin-top: 0;
}
.social__welcome .icon-close {
float:right;
}
.social__welcome .social-id {
font-weight: bold;
}
.social__timeline {
max-width: 700px;
margin: 15px auto;
}
.new-post {
display: flex;
padding: 10px;
background-color: var(--color-main-background);
position: sticky;
top: 47px;
z-index: 100;
margin-bottom: 10px;
}
.new-post-author {
padding: 5px;
}
.author .social-id {
opacity: .5;
}
.new-post-form {
flex-grow: 1;
position: relative;
}
.message {
width: 100%;
}
[contenteditable=true]:empty:before{
content: attr(placeholder);
display: block; /* For Firefox */
opacity: .5;
}
input[type=submit] {
width: 44px;
height: 44px;
margin: 0;
padding: 13px;
background-color: transparent;
border: none;
opacity: 0.3;
position: absolute;
bottom: 0;
right: 0;
}
#app-content {
position: relative;
}
</style>
<script>
import {
PopoverMenu,
AppNavigation,
Multiselect,
Avatar
} from 'nextcloud-vue'
import TimelineEntry from './../components/TimelineEntry'
import ProfileInfo from './../components/ProfileInfo'
export default {
name: 'Profile',
components: {
PopoverMenu,
AppNavigation,
TimelineEntry,
Multiselect,
Avatar,
ProfileInfo
},
data: function() {
return {
state: [],
uid: null
}
},
computed: {
serverData: function() {
return this.$store.getters.getServerData
},
currentUser: function() {
return OC.getCurrentUser()
},
socialId: function() {
return '@' + OC.getCurrentUser().uid + '@' + OC.getHost()
},
timeline: function() {
return this.$store.getters.getTimeline
}
},
beforeMount() {
this.uid = this.$route.params.account
this.$store.dispatch('fetchAccountInfo', this.uid)
},
methods: {
}
}
</script>

Wyświetl plik

@ -0,0 +1,52 @@
<!--
- @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/>.
-
-->
<template>
<div class="social__timeline">
<ul>
<li>User List</li>
</ul>
</div>
</template>
<style scoped>
.social__timeline {
max-width: 700px;
margin: 15px auto;
}
</style>
<script>
import TimelineEntry from './../components/TimelineEntry'
export default {
name: 'ProfileFollowers',
components: {
TimelineEntry
},
computed: {
timeline: function() {
return this.$store.getters.getTimeline
}
}
}
</script>

Wyświetl plik

@ -0,0 +1,50 @@
<!--
- @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/>.
-
-->
<template>
<div class="social__timeline">
<timeline-entry v-for="entry in timeline" :item="entry" />
</div>
</template>
<style scoped>
.social__timeline {
max-width: 700px;
margin: 15px auto;
}
</style>
<script>
import TimelineEntry from './../components/TimelineEntry'
export default {
name: 'ProfileTimeline',
components: {
TimelineEntry
},
computed: {
timeline: function() {
return this.$store.getters.getTimeline
}
}
}
</script>

Wyświetl plik

@ -0,0 +1,224 @@
<template>
<div class="social__wrapper">
<div class="social__container">
<div v-if="!infoHidden" class="social__welcome">
<a class="close icon-close" href="#" @click="hideInfo()"><span class="hidden-visually">Close</span></a>
<h3>🎉{{ t('social', 'Nextcloud becomes part of the federated social networks!') }}</h3>
<p>
{{ t('social', 'We have automatically created a social account for you. Your social id is the same as the federated cloud id:') }}
<span class="social-id">{{ socialId }}</span>
</p>
</div>
<div class="social__timeline">
<div class="new-post" data-id="">
<div class="new-post-author">
<avatar :user="currentUser.uid" :display-name="currentUser.displayName" :size="32" />
</div>
<form class="new-post-form">
<div class="author currentUser">
{{ currentUser.displayName }}
<span class="social-id">{{ socialId }}</span>
</div>
<div contenteditable="true" class="message" placeholder="Share a thought…" />
<input class="submit icon-confirm has-tooltip" type="submit" value=""
title="" data-original-title="Post">
<div class="submitLoading icon-loading-small hidden" />
</form>
</div>
<timeline-entry v-for="entry in timeline" :item="entry" />
</div>
</div>
</div>
</template>
<style scoped>
.social__wrapper {
display: flex;
}
.social__container {
flex-grow: 1;
}
.social__profile {
max-width: 500px;
flex-grow: 1;
border-right: 1px solid var(--color-background-dark);
text-align: center;
padding-top: 20px;
}
.social__welcome {
max-width: 700px;
margin: 15px auto;
padding: 15px;
border-radius: 10px;
background-color: var(--color-background-dark);
}
.social__welcome h3 {
margin-top: 0;
}
.social__welcome .icon-close {
float:right;
}
.social__welcome .social-id {
font-weight: bold;
}
.social__timeline {
max-width: 700px;
margin: 15px auto;
}
.new-post {
display: flex;
padding: 10px;
background-color: var(--color-main-background);
position: sticky;
top: 47px;
z-index: 100;
margin-bottom: 10px;
}
.new-post-author {
padding: 5px;
}
.author .social-id {
opacity: .5;
}
.new-post-form {
flex-grow: 1;
position: relative;
}
.message {
width: 100%;
}
[contenteditable=true]:empty:before{
content: attr(placeholder);
display: block; /* For Firefox */
opacity: .5;
}
input[type=submit] {
width: 44px;
height: 44px;
margin: 0;
padding: 13px;
background-color: transparent;
border: none;
opacity: 0.3;
position: absolute;
bottom: 0;
right: 0;
}
#app-content {
position: relative;
}
</style>
<script>
import {
PopoverMenu,
AppNavigation,
Multiselect,
Avatar
} from 'nextcloud-vue'
import TimelineEntry from './../components/TimelineEntry'
export default {
name: 'Timeline',
components: {
PopoverMenu, AppNavigation, TimelineEntry, Multiselect, Avatar
},
data: function() {
return {
infoHidden: false,
state: []
}
},
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
},
menu: function() {
let defaultCategories = [
{
id: 'social-timeline',
classes: [],
href: '#',
icon: 'icon-category-monitoring',
text: t('social', 'Timeline')
},
{
id: 'social-account',
classes: [],
href: '#',
icon: 'icon-category-user',
text: t('social', 'Your account')
},
{
id: 'social-friends',
classes: [],
href: '#',
icon: 'icon-category-social',
text: t('social', 'Friends')
},
{
id: 'social-favorites',
classes: [],
href: '#',
icon: 'icon-favorite',
text: t('social', 'Favorites')
},
{
id: 'social-direct-messages',
classes: [],
href: '#',
icon: 'icon-comment',
utils: {
counter: 3
},
text: t('social', 'Direct messages')
}
]
return {
items: defaultCategories,
loading: false
}
}
},
beforeMount: function() {
let example = {
message: 'Want to #DropDropbox? #DeleteGoogle? #decentralize? We got you covered, easy as a piece of 🥞\n'
+ '\n'
+ 'Get started right now: https://nextcloud.com/signup',
author: 'Nextcloud 📱☁️💻',
authorId: '@nextcloud@mastodon.xyz',
authorAvatar: OC.linkTo('social', 'img/nextcloud.png'),
timestamp: '1 day ago'
}
let data = []
for (let i = 0; i < 20; i++) {
let item = Object.assign({}, example)
item.id = i
data.push(item)
}
this.$store.commit('addToTimeline', data)
},
methods: {
hideInfo() {
this.infoHidden = true
}
}
}
</script>

Wyświetl plik

@ -1 +0,0 @@
ACTOR !

Wyświetl plik

@ -1 +0,0 @@
FOLLOWERS !!!1

Wyświetl plik

@ -1 +0,0 @@
FOLLOWING

Wyświetl plik

@ -2,4 +2,5 @@
script('social', 'social');
style('social', 'style');
?>
<span id="serverData" data-server="<?php p(json_encode($_['serverData']));?>"></span>
<div id="vue-content"></div>

Wyświetl plik

@ -8,5 +8,5 @@ module.exports = merge(common, {
noInfo: true,
overlay: true
},
devtool: '#eval-source-map',
devtool: 'source-map',
})