kopia lustrzana https://github.com/nextcloud/social
commit
3d439139d2
lib
src
mixins
store
templates
|
@ -0,0 +1,3 @@
|
|||
const babelConfig = require('@nextcloud/babel-config')
|
||||
|
||||
module.exports = babelConfig
|
|
@ -37,11 +37,13 @@ use OCA\Social\Search\UnifiedSearchProvider;
|
|||
use OCA\Social\Service\ConfigService;
|
||||
use OCA\Social\Service\UpdateService;
|
||||
use OCA\Social\WellKnown\WebfingerHandler;
|
||||
use OCA\Social\Listeners\ProfileSectionListener;
|
||||
use OCP\AppFramework\App;
|
||||
use OCP\AppFramework\Bootstrap\IBootContext;
|
||||
use OCP\AppFramework\Bootstrap\IBootstrap;
|
||||
use OCP\AppFramework\Bootstrap\IRegistrationContext;
|
||||
use OCP\AppFramework\QueryException;
|
||||
use OCP\Profile\BeforeTemplateRenderedEvent;
|
||||
use OCP\IDBConnection;
|
||||
use OCP\IServerContainer;
|
||||
use OC\DB\SchemaWrapper;
|
||||
|
@ -62,19 +64,12 @@ class Application extends App implements IBootstrap {
|
|||
parent::__construct(self::APP_NAME, $params);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param IRegistrationContext $context
|
||||
*/
|
||||
public function register(IRegistrationContext $context): void {
|
||||
$context->registerSearchProvider(UnifiedSearchProvider::class);
|
||||
$context->registerWellKnownHandler(WebfingerHandler::class);
|
||||
$context->registerEventListener(BeforeTemplateRenderedEvent::class, ProfileSectionListener::class);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param IBootContext $context
|
||||
*/
|
||||
public function boot(IBootContext $context): void {
|
||||
$manager = $context->getServerContainer()
|
||||
->getNotificationManager();
|
||||
|
|
|
@ -124,18 +124,26 @@ class LocalController extends Controller {
|
|||
*
|
||||
* @NoAdminRequired
|
||||
*/
|
||||
public function postCreate(array $data): DataResponse {
|
||||
public function postCreate(string $content = '', $to = null, string $type = null, ?string $replyTo = null, $attachments = null, ?string $hashtags = null): DataResponse {
|
||||
$content = $content ?? '';
|
||||
$to = is_string($to) ? [$to] : $to;
|
||||
$to = $to ?? [];
|
||||
$replyTo = $replyTo ?? '';
|
||||
$type = $type ?? Stream::TYPE_PUBLIC;
|
||||
$hashtags = $hashtags === '' ? [] : $hashtags;
|
||||
$hashtags = $hashtags ?? [];
|
||||
$attachments = $attachments ?? [];
|
||||
|
||||
try {
|
||||
$actor = $this->accountService->getActorFromUserId($this->userId);
|
||||
|
||||
$post = new Post($actor);
|
||||
$post->setContent($this->get('content', $data, ''));
|
||||
$post->setReplyTo($this->get('replyTo', $data, ''));
|
||||
$post->setTo($this->getArray('to', $data, []));
|
||||
$post->addTo($this->get('to', $data, ''));
|
||||
$post->setType($this->get('type', $data, Stream::TYPE_PUBLIC));
|
||||
$post->setHashtags($this->getArray('hashtags', $data, []));
|
||||
$post->setAttachments($this->getArray('attachments', $data, []));
|
||||
$post->setContent($content);
|
||||
$post->setReplyTo($replyTo);
|
||||
$post->setTo($to);
|
||||
$post->setType($type);
|
||||
$post->setHashtags($hashtags);
|
||||
$post->setAttachments($attachments);
|
||||
|
||||
$token = '';
|
||||
$activity = $this->postService->createPost($post, $token);
|
||||
|
@ -151,7 +159,6 @@ class LocalController extends Controller {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get info about a post (limited to viewer rights).
|
||||
*
|
||||
|
|
|
@ -46,7 +46,6 @@ use OCA\Social\Model\StreamAction;
|
|||
use OCA\Social\Service\ConfigService;
|
||||
use OCA\Social\Service\MiscService;
|
||||
use OCP\DB\QueryBuilder\IQueryBuilder;
|
||||
use OCP\DB\QueryBuilder\IQueryFunction;
|
||||
use OCP\IDBConnection;
|
||||
use OCP\IURLGenerator;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
@ -584,7 +583,7 @@ class CoreRequestBuilder {
|
|||
protected function exprLimitToDBField(
|
||||
IQueryBuilder &$qb, string $field, string $value, bool $eq = true, bool $cs = true,
|
||||
string $alias = ''
|
||||
): IQueryFunction {
|
||||
): string {
|
||||
$expr = $qb->expr();
|
||||
|
||||
$pf = '';
|
||||
|
@ -618,7 +617,7 @@ class CoreRequestBuilder {
|
|||
|
||||
protected function exprLimitToDBFieldInt(
|
||||
IQueryBuilder &$qb, string $field, int $value, string $alias = ''
|
||||
): IQueryFunction {
|
||||
): string {
|
||||
$expr = $qb->expr();
|
||||
|
||||
$pf = '';
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
// SPDX-FileCopyrightText: 2022 Carl Schwan <carl@carlschwan.eu>
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
namespace OCA\Social\Listeners;
|
||||
|
||||
use OCP\EventDispatcher\Event;
|
||||
use OCP\EventDispatcher\IEventListener;
|
||||
use OCP\Profile\BeforeTemplateRenderedEvent;
|
||||
use OCP\Util;
|
||||
|
||||
class ProfileSectionListener implements IEventListener {
|
||||
public function handle(Event $event): void {
|
||||
if (!($event instanceof BeforeTemplateRenderedEvent)) {
|
||||
return;
|
||||
}
|
||||
Util::addScript('social', 'social-profilePage');
|
||||
}
|
||||
}
|
|
@ -137,6 +137,24 @@ class CacheDocumentService {
|
|||
$document->setResizedCopy($resized);
|
||||
}
|
||||
|
||||
public function saveFromTempToCache(Document $document, string $tmpPath) {
|
||||
$mime = mime_content_type($tmpPath);
|
||||
|
||||
$this->filterMimeTypes($mime);
|
||||
|
||||
$document->setMediaType($mime);
|
||||
$document->setMimeType($mime);
|
||||
|
||||
$file = fopen($tmpPath, 'r');
|
||||
$content = fread($file, filesize($tmpPath));
|
||||
|
||||
$filename = $this->generateFileFromContent($content);
|
||||
$document->setLocalCopy($filename);
|
||||
$this->resizeImage($content);
|
||||
$resized = $this->generateFileFromContent($content);
|
||||
$document->setResizedCopy($resized);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param string $content
|
||||
|
|
|
@ -53,34 +53,20 @@ use OCA\Social\Model\ActivityPub\Object\Note;
|
|||
use OCA\Social\Model\Post;
|
||||
use OCP\Files\NotFoundException;
|
||||
use OCP\Files\NotPermittedException;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
class PostService {
|
||||
private StreamService $streamService;
|
||||
|
||||
private AccountService $accountService;
|
||||
|
||||
private ActivityService $activityService;
|
||||
|
||||
private CacheDocumentService $cacheDocumentService;
|
||||
|
||||
private ConfigService $configService;
|
||||
|
||||
private MiscService $miscService;
|
||||
private LoggerInterface $logger;
|
||||
|
||||
|
||||
/**
|
||||
* PostService constructor.
|
||||
*
|
||||
* @param StreamService $streamService
|
||||
* @param AccountService $accountService
|
||||
* @param ActivityService $activityService
|
||||
* @param CacheDocumentService $cacheDocumentService
|
||||
* @param ConfigService $configService
|
||||
* @param MiscService $miscService
|
||||
*/
|
||||
public function __construct(
|
||||
StreamService $streamService, AccountService $accountService, ActivityService $activityService,
|
||||
CacheDocumentService $cacheDocumentService, ConfigService $configService, MiscService $miscService
|
||||
CacheDocumentService $cacheDocumentService, ConfigService $configService, MiscService $miscService, LoggerInterface $logger
|
||||
) {
|
||||
$this->streamService = $streamService;
|
||||
$this->accountService = $accountService;
|
||||
|
@ -88,6 +74,7 @@ class PostService {
|
|||
$this->cacheDocumentService = $cacheDocumentService;
|
||||
$this->configService = $configService;
|
||||
$this->miscService = $miscService;
|
||||
$this->logger = $logger;
|
||||
}
|
||||
|
||||
|
||||
|
@ -142,15 +129,48 @@ class PostService {
|
|||
*/
|
||||
private function generateDocumentsFromAttachments(Note $note, Post $post) {
|
||||
$documents = [];
|
||||
foreach ($post->getAttachments() as $attachment) {
|
||||
\OC::$server->getLogger()->error(var_export($_FILES["attachments"], true));
|
||||
if (!isset($_FILES['attachments'])) {
|
||||
return [];
|
||||
}
|
||||
if (is_array($_FILES["attachments"]["error"])) {
|
||||
foreach ($_FILES["attachments"]["error"] as $key => $error) {
|
||||
if ($error == UPLOAD_ERR_OK) {
|
||||
try {
|
||||
$document = $this->generateDocumentFromAttachment($note, $key);
|
||||
|
||||
$service = AP::$activityPub->getInterfaceForItem($document);
|
||||
$service->save($document);
|
||||
|
||||
$documents[] = $document;
|
||||
} catch (Exception $e) {
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
$document = $this->generateDocumentFromAttachment($note, $attachment);
|
||||
$tmp_name = $_FILES["attachments"]["tmp_name"];
|
||||
$name = basename($_FILES["attachments"]["name"]);
|
||||
$tmpFile = tmpfile();
|
||||
$tmpPath = stream_get_meta_data($tmpFile)['uri'];
|
||||
if (move_uploaded_file($tmp_name, $tmpPath)) {
|
||||
$document = new Document();
|
||||
$document->setUrlCloud($this->configService->getCloudUrl());
|
||||
$document->generateUniqueId('/documents/local');
|
||||
$document->setParentId($note->getId());
|
||||
$document->setPublic(true);
|
||||
|
||||
$this->cacheDocumentService->saveFromTempToCache($document, $tmpPath);
|
||||
}
|
||||
|
||||
$service = AP::$activityPub->getInterfaceForItem($document);
|
||||
$service->save($document);
|
||||
|
||||
$documents[] = $document;
|
||||
} catch (Exception $e) {
|
||||
$this->logger->error($e->getMessage(), [
|
||||
'exception' => $e,
|
||||
]);
|
||||
}
|
||||
}
|
||||
$post->setDocuments($documents);
|
||||
|
@ -168,21 +188,21 @@ class PostService {
|
|||
* @throws SocialAppConfigException
|
||||
* @throws UrlCloudException
|
||||
*/
|
||||
private function generateDocumentFromAttachment(Note $note, string $attachment): Document {
|
||||
list(, $data) = explode(';', $attachment);
|
||||
list(, $data) = explode(',', $data);
|
||||
$content = base64_decode($data);
|
||||
private function generateDocumentFromAttachment(Note $note, int $key): Document {
|
||||
$tmp_name = $_FILES["attachments"]["tmp_name"][$key];
|
||||
$name = basename($_FILES["attachments"]["name"][$key]);
|
||||
$tmpFile = tmpfile();
|
||||
$tmpPath = stream_get_meta_data($tmpFile)['uri'];
|
||||
if (move_uploaded_file($tmp_name, $tmpPath)) {
|
||||
$document = new Document();
|
||||
$document->setUrlCloud($this->configService->getCloudUrl());
|
||||
$document->generateUniqueId('/documents/local');
|
||||
$document->setParentId($note->getId());
|
||||
$document->setPublic(true);
|
||||
|
||||
$document = new Document();
|
||||
$document->setUrlCloud($this->configService->getCloudUrl());
|
||||
$document->generateUniqueId('/documents/local');
|
||||
$document->setParentId($note->getId());
|
||||
$document->setPublic(true);
|
||||
$this->cacheDocumentService->saveFromTempToCache($document, $tmpPath);
|
||||
}
|
||||
|
||||
$mime = '';
|
||||
$this->cacheDocumentService->saveLocalUploadToCache($document, $content, $mime);
|
||||
$document->setMediaType($mime);
|
||||
$document->setMimeType($mime);
|
||||
|
||||
return $document;
|
||||
}
|
||||
|
|
|
@ -42,7 +42,6 @@ use Exception;
|
|||
use OC\DB\QueryBuilder\QueryBuilder;
|
||||
use OCP\DB\QueryBuilder\ICompositeExpression;
|
||||
use OCP\DB\QueryBuilder\IQueryBuilder;
|
||||
use OCP\DB\QueryBuilder\IQueryFunction;
|
||||
|
||||
/**
|
||||
* Class ExtendedQueryBuilder
|
||||
|
@ -174,7 +173,7 @@ class ExtendedQueryBuilder extends QueryBuilder implements IExtendedQueryBuilder
|
|||
|
||||
public function exprLimitToDBField(
|
||||
string $field, string $value, bool $eq = true, bool $cs = true, string $alias = ''
|
||||
): IQueryFunction {
|
||||
): string {
|
||||
$expr = $this->expr();
|
||||
|
||||
$pf = '';
|
||||
|
@ -295,7 +294,7 @@ class ExtendedQueryBuilder extends QueryBuilder implements IExtendedQueryBuilder
|
|||
}
|
||||
|
||||
public function exprLimitToDBFieldInt(string $field, int $value, string $alias = '', bool $eq = true
|
||||
): IQueryFunction {
|
||||
): string {
|
||||
$expr = $this->expr();
|
||||
|
||||
$pf = '';
|
||||
|
|
|
@ -35,7 +35,6 @@ use DateTime;
|
|||
use Exception;
|
||||
use OCP\DB\QueryBuilder\ICompositeExpression;
|
||||
use OCP\DB\QueryBuilder\IQueryBuilder;
|
||||
use OCP\DB\QueryBuilder\IQueryFunction;
|
||||
|
||||
/**
|
||||
* Interface IExtendedQueryBuilder
|
||||
|
@ -134,7 +133,7 @@ interface IExtendedQueryBuilder extends IQueryBuilder {
|
|||
|
||||
public function exprLimitToDBField(
|
||||
string $field, string $value, bool $eq = true, bool $cs = true, string $alias = ''
|
||||
): IQueryFunction;
|
||||
): string;
|
||||
|
||||
public function limitToDBFieldArray(
|
||||
string $field, array $values, bool $cs = true, string $alias = ''
|
||||
|
@ -191,7 +190,7 @@ interface IExtendedQueryBuilder extends IQueryBuilder {
|
|||
* @param int $value
|
||||
* @param string $alias
|
||||
*/
|
||||
public function exprLimitToDBFieldInt(string $field, int $value, string $alias = ''): IQueryFunction;
|
||||
public function exprLimitToDBFieldInt(string $field, int $value, string $alias = ''): string;
|
||||
|
||||
|
||||
/**
|
||||
|
|
Plik diff jest za duży
Load Diff
70
package.json
70
package.json
|
@ -17,9 +17,9 @@
|
|||
"license": "agpl",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "NODE_ENV=development webpack --config webpack.dev.js",
|
||||
"watch": "NODE_ENV=development webpack --progress --watch --config webpack.dev.js",
|
||||
"build": "NODE_ENV=production webpack --progress --config webpack.prod.js",
|
||||
"dev": "NODE_ENV=development webpack --config webpack.common.js",
|
||||
"watch": "NODE_ENV=development webpack --progress --watch --config webpack.common.js",
|
||||
"build": "NODE_ENV=production webpack --progress --config webpack.common.js",
|
||||
"lint": "eslint --ext .js,.vue src",
|
||||
"lint:fix": "eslint --ext .js,.vue src --fix",
|
||||
"test": "jest",
|
||||
|
@ -28,21 +28,22 @@
|
|||
"cypress:gui": "cypress open"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.17.9",
|
||||
"@nextcloud/auth": "^1.3.0",
|
||||
"@nextcloud/axios": "^1.9.0",
|
||||
"@nextcloud/initial-state": "^1.2.1",
|
||||
"@nextcloud/auth": "^2.0.0",
|
||||
"@nextcloud/axios": "^2.0.0",
|
||||
"@nextcloud/initial-state": "^2.0.0",
|
||||
"@nextcloud/logger": "^2.2.1",
|
||||
"@nextcloud/moment": "^1.1.1",
|
||||
"@nextcloud/moment": "^1.2.1",
|
||||
"@nextcloud/router": "^1.2.0",
|
||||
"@nextcloud/vue": "^5.3.1",
|
||||
"@nextcloud/stylelint-config": "^2.2.0",
|
||||
"@nextcloud/vue": "^7.0.0-beta.2",
|
||||
"@nextcloud/vue-richtext": "^2.0.0",
|
||||
"he": "^1.2.0",
|
||||
"linkifyjs": "^2.1.8",
|
||||
"tributejs": "^3.7.3",
|
||||
"twemoji": "12.0.1",
|
||||
"uuid": "^8.3.2",
|
||||
"v-tooltip": "^3.0.0-alpha.21",
|
||||
"vue": "^2.6.14",
|
||||
"vue": "^2.7.10",
|
||||
"vue-click-outside": "^1.0.7",
|
||||
"vue-contenteditable-directive": "^1.2.0",
|
||||
"vue-infinite-loading": "^2.4.4",
|
||||
|
@ -55,55 +56,24 @@
|
|||
"vuex-router-sync": "^5.0.0"
|
||||
},
|
||||
"browserslist": [
|
||||
"last 2 versions",
|
||||
"not ie <= 11"
|
||||
"extends @nextcloud/browserslist-config"
|
||||
],
|
||||
"browser": {
|
||||
"fs": false
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.0.0",
|
||||
"npm": "^7.0.0"
|
||||
"node": "^16.0.0",
|
||||
"npm": "^7.0.0 || ^8.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.17.9",
|
||||
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
|
||||
"@babel/preset-env": "^7.18.10",
|
||||
"@nextcloud/babel-config": "^1.0.0",
|
||||
"@nextcloud/browserslist-config": "^2.2.0",
|
||||
"@nextcloud/eslint-config": "^8.0.0",
|
||||
"@nextcloud/eslint-plugin": "^1.5.0",
|
||||
"@vue/test-utils": "^1.1.3",
|
||||
"acorn": "^8.1.0",
|
||||
"babel-eslint": "^10.0.3",
|
||||
"babel-jest": "^28.1.2",
|
||||
"babel-loader": "^8.2.5",
|
||||
"css-loader": "^5.2.7",
|
||||
"cypress": "^9.6.1",
|
||||
"cypress-image-snapshot": "^4.0.1",
|
||||
"cypress-testing-library": "^4.0.0",
|
||||
"eslint": "^6.8.0",
|
||||
"eslint-config-standard": "^12.0.0",
|
||||
"eslint-friendly-formatter": "^4.0.1",
|
||||
"eslint-loader": "^3.0.2",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-promise": "^4.3.1",
|
||||
"eslint-plugin-standard": "^5.0.0",
|
||||
"eslint-plugin-vue": "^6.2.2",
|
||||
"file-loader": "^6.2.0",
|
||||
"@nextcloud/webpack-vue-config": "^5.3.0",
|
||||
"jest": "^26.6.3",
|
||||
"jest-serializer-vue": "^2.0.2",
|
||||
"node-sass": "^5.0.0",
|
||||
"prettier-eslint": "^14.0.3",
|
||||
"raw-loader": "^4.0.2",
|
||||
"sass-loader": "^10.1.1",
|
||||
"style-loader": "^2.0.0",
|
||||
"stylelint": "^8.4.0",
|
||||
"stylelint-config-recommended-scss": "^3.3.0",
|
||||
"stylelint-webpack-plugin": "^0.10.5",
|
||||
"vue-jest": "^3.0.7",
|
||||
"vue-loader": "^15.9.6",
|
||||
"vue-template-compiler": "^2.6.14",
|
||||
"webpack": "^4.46.0",
|
||||
"webpack-cli": "^4.9.2",
|
||||
"webpack-merge": "^5.8.0"
|
||||
"vue-template-compiler": "^2.7.10"
|
||||
},
|
||||
"jest": {
|
||||
"moduleFileExtensions": [
|
||||
|
|
114
src/App.vue
114
src/App.vue
|
@ -1,14 +1,16 @@
|
|||
<template>
|
||||
<Content v-if="!serverData.setup" app-name="social" :class="{public: serverData.public}">
|
||||
<AppNavigation v-if="!serverData.public">
|
||||
<AppNavigationItem v-for="item in menu.items" :key="item.key" :to="item.to"
|
||||
:title="item.title" :exact="true">
|
||||
<template #icon>
|
||||
<component :is="item.icon" />
|
||||
</template>
|
||||
</AppNavigationItem>
|
||||
</AppNavigation>
|
||||
<AppContent>
|
||||
<NcContent v-if="!serverData.setup" app-name="social" :class="{public: serverData.public}">
|
||||
<NcAppNavigation v-if="!serverData.public">
|
||||
<template #list>
|
||||
<NcAppNavigationItem v-for="item in menu.items" :key="item.key" :to="item.to"
|
||||
:title="item.title" :exact="true">
|
||||
<template #icon>
|
||||
<component :is="item.icon" />
|
||||
</template>
|
||||
</NcAppNavigationItem>
|
||||
</template>
|
||||
</NcAppNavigation>
|
||||
<NcAppContent>
|
||||
<div v-if="serverData.isAdmin && !serverData.checks.success" class="setup social__wrapper">
|
||||
<h3 v-if="!serverData.checks.checks.wellknown">
|
||||
{{ t('social', '.well-known/webfinger isn\'t properly set up!') }}
|
||||
|
@ -24,10 +26,10 @@
|
|||
</div>
|
||||
<Search v-if="searchTerm !== ''" :term="searchTerm" />
|
||||
<router-view v-if="searchTerm === ''" :key="$route.fullPath" />
|
||||
</AppContent>
|
||||
</Content>
|
||||
<Content v-else app-name="social">
|
||||
<AppContent v-if="serverData.isAdmin" class="setup">
|
||||
</NcAppContent>
|
||||
</NcContent>
|
||||
<NcContent v-else app-name="social">
|
||||
<NcAppContent v-if="serverData.isAdmin" class="setup">
|
||||
<h2>{{ t('social', 'Social app setup') }}</h2>
|
||||
<p>{{ t('social', 'ActivityPub requires a fixed URL to make entries unique. Note that this cannot be changed later without resetting the Social app.') }}</p>
|
||||
<form @submit.prevent="setCloudAddress">
|
||||
|
@ -55,47 +57,18 @@
|
|||
</p>
|
||||
</template>
|
||||
</form>
|
||||
</AppContent>
|
||||
<AppContent v-else class="setup">
|
||||
</NcAppContent>
|
||||
<NcAppContent v-else class="setup">
|
||||
<p>{{ t('social', 'The Social app needs to be set up by the server administrator.') }}</p>
|
||||
</AppContent>
|
||||
</Content>
|
||||
</NcAppContent>
|
||||
</NcContent>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
#app-content-vue .social__wrapper {
|
||||
padding: 15px;
|
||||
max-width: 630px;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.setup {
|
||||
margin: 0 auto !important;
|
||||
padding: 15px;
|
||||
max-width: 630px;
|
||||
}
|
||||
|
||||
.setup input[type=url] {
|
||||
width: 300px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
#social-spacer a:hover,
|
||||
#social-spacer a:focus {
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
a.external_link {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import Content from '@nextcloud/vue/dist/Components/Content'
|
||||
import AppContent from '@nextcloud/vue/dist/Components/AppContent'
|
||||
import AppNavigation from '@nextcloud/vue/dist/Components/AppNavigation'
|
||||
import AppNavigationItem from '@nextcloud/vue/dist/Components/AppNavigationItem'
|
||||
import NcContent from '@nextcloud/vue/dist/Components/NcContent.js'
|
||||
import NcAppContent from '@nextcloud/vue/dist/Components/NcAppContent.js'
|
||||
import NcAppNavigation from '@nextcloud/vue/dist/Components/NcAppNavigation.js'
|
||||
import NcAppNavigationItem from '@nextcloud/vue/dist/Components/NcAppNavigationItem.js'
|
||||
|
||||
import Home from 'vue-material-design-icons/Home.vue'
|
||||
import CommentAccount from 'vue-material-design-icons/CommentAccount.vue'
|
||||
|
@ -107,17 +80,17 @@ import Earth from 'vue-material-design-icons/Earth.vue'
|
|||
|
||||
import axios from '@nextcloud/axios'
|
||||
import Search from './components/Search.vue'
|
||||
import currentuserMixin from './mixins/currentUserMixin'
|
||||
import currentuserMixin from './mixins/currentUserMixin.js'
|
||||
import { loadState } from '@nextcloud/initial-state'
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
|
||||
export default {
|
||||
name: 'App',
|
||||
components: {
|
||||
Content,
|
||||
AppContent,
|
||||
AppNavigation,
|
||||
AppNavigationItem,
|
||||
NcContent,
|
||||
NcAppContent,
|
||||
NcAppNavigation,
|
||||
NcAppNavigationItem,
|
||||
Search,
|
||||
},
|
||||
mixins: [currentuserMixin],
|
||||
|
@ -258,3 +231,32 @@ export default {
|
|||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
#app-content-vue .social__wrapper {
|
||||
padding: 15px;
|
||||
max-width: 800px;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.setup {
|
||||
margin: 0 auto !important;
|
||||
padding: 15px;
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
.setup input[type=url] {
|
||||
width: 300px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
#social-spacer a:hover,
|
||||
#social-spacer a:focus {
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
a.external_link {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
|
@ -21,20 +21,20 @@
|
|||
-->
|
||||
|
||||
<template>
|
||||
<avatar v-if="actor.local" :size="size" :user="actor.preferredUsername"
|
||||
:display-name="actor.account" :disable-tooltip="true" />
|
||||
<avatar v-else :size="size" :url="avatarUrl"
|
||||
<NcAvatar v-if="actor.local" :size="size" :user="actor.preferredUsername"
|
||||
:display-name="actor.account" :disable-tooltip="true" :showUserStatus="false" />
|
||||
<NcAvatar v-else :size="size" :url="avatarUrl" :showUserStatus="false"
|
||||
:disable-tooltip="true" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Avatar from '@nextcloud/vue/dist/Components/Avatar'
|
||||
import NcAvatar from '@nextcloud/vue/dist/Components/NcAvatar.js'
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
|
||||
export default {
|
||||
name: 'ActorAvatar',
|
||||
components: {
|
||||
Avatar
|
||||
NcAvatar
|
||||
},
|
||||
props: {
|
||||
actor: { type: Object, default: () => {} },
|
||||
|
|
|
@ -32,7 +32,7 @@
|
|||
aria-hidden="true"
|
||||
class="hidden-visually">
|
||||
<div class="new-post-author">
|
||||
<avatar :user="currentUser.uid" :display-name="currentUser.displayName" :disable-tooltip="true"
|
||||
<NcAvatar :user="currentUser.uid" :display-name="currentUser.displayName" :disable-tooltip="true"
|
||||
:size="32" />
|
||||
<div class="post-author">
|
||||
<span class="post-author-name">
|
||||
|
@ -44,11 +44,17 @@
|
|||
</div>
|
||||
</div>
|
||||
<div v-if="replyTo" class="reply-to">
|
||||
<p>
|
||||
<p class="reply-info">
|
||||
<span>{{ t('social', 'In reply to') }}</span>
|
||||
<actor-avatar :actor="replyTo.actor_info" :size="16" />
|
||||
<strong>{{ replyTo.actor_info.account }}</strong>
|
||||
<a class="icon-close" @click="closeReply()" />
|
||||
<NcButton type="tertiary" class="close-button"
|
||||
@click="closeReply"
|
||||
:aria-label="t('social', 'Close reply')">
|
||||
<template #icon>
|
||||
<Close :size="20" />
|
||||
</template>
|
||||
</NcButton>
|
||||
</p>
|
||||
<div class="reply-to-preview">
|
||||
{{ replyTo.content }}
|
||||
|
@ -65,48 +71,48 @@
|
|||
<PreviewGrid :uploading="false" :uploadProgress="0.4" :miniatures="previewUrls" />
|
||||
|
||||
<div class="options">
|
||||
<Button type="tertiary"
|
||||
<NcButton type="tertiary"
|
||||
@click.prevent="clickImportInput"
|
||||
:aria-label="t('social', 'Add attachment')"
|
||||
v-tooltip="t('social', 'Add attachment')">
|
||||
<template #icon>
|
||||
<FileUpload :size="22" decorative title="" />
|
||||
</template>
|
||||
</Button>
|
||||
</NcButton>
|
||||
|
||||
<div class="new-post-form__emoji-picker">
|
||||
<EmojiPicker ref="emojiPicker" :search="search" :close-on-select="false"
|
||||
<NcEmojiPicker ref="emojiPicker" :search="search" :close-on-select="false"
|
||||
:container="container"
|
||||
@select="insert">
|
||||
<Button type="tertiary"
|
||||
<NcButton type="tertiary"
|
||||
:aria-haspopup="true"
|
||||
:aria-label="t('social', 'Add emoji')"
|
||||
v-tooltip="t('social', 'Add emoji')">
|
||||
<template #icon>
|
||||
<EmoticonOutline :size="22" decorative title="" />
|
||||
</template>
|
||||
</Button>
|
||||
</EmojiPicker>
|
||||
</NcButton>
|
||||
</NcEmojiPicker>
|
||||
</div>
|
||||
|
||||
<div v-click-outside="hidePopoverMenu" class="popovermenu-parent">
|
||||
<Button type="tertiary"
|
||||
<NcButton type="tertiary"
|
||||
:class="currentVisibilityIconClass"
|
||||
@click.prevent="togglePopoverMenu"
|
||||
v-tooltip="t('social', 'Visibility')" />
|
||||
<div :class="{open: menuOpened}" class="popovermenu">
|
||||
<popover-menu :menu="visibilityPopover" />
|
||||
<NcPopoverMenu :menu="visibilityPopover" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="emptySpace" />
|
||||
<Button :value="currentVisibilityPostLabel" :disabled="!canPost" type="primary"
|
||||
<NcButton :value="currentVisibilityPostLabel" :disabled="!canPost" type="primary"
|
||||
@click.prevent="createPost">
|
||||
<template #icon>
|
||||
<Send title="" :size="22" decorative />
|
||||
</template>
|
||||
<template>{{ postTo }}</template>
|
||||
</button>
|
||||
</NcButton>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
@ -114,34 +120,36 @@
|
|||
|
||||
<script>
|
||||
|
||||
import EmoticonOutline from 'vue-material-design-icons/EmoticonOutline'
|
||||
import Send from 'vue-material-design-icons/Send'
|
||||
import FileUpload from 'vue-material-design-icons/FileUpload'
|
||||
import Avatar from '@nextcloud/vue/dist/Components/Avatar'
|
||||
import Button from '@nextcloud/vue/dist/Components/Button'
|
||||
import PopoverMenu from '@nextcloud/vue/dist/Components/PopoverMenu'
|
||||
import EmojiPicker from '@nextcloud/vue/dist/Components/EmojiPicker'
|
||||
import EmoticonOutline from 'vue-material-design-icons/EmoticonOutline.vue'
|
||||
import Send from 'vue-material-design-icons/Send.vue'
|
||||
import Close from 'vue-material-design-icons/Close.vue'
|
||||
import FileUpload from 'vue-material-design-icons/FileUpload.vue'
|
||||
import NcAvatar from '@nextcloud/vue/dist/Components/NcAvatar.js'
|
||||
import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
|
||||
import NcPopoverMenu from '@nextcloud/vue/dist/Components/NcPopoverMenu.js'
|
||||
import NcEmojiPicker from '@nextcloud/vue/dist/Components/NcEmojiPicker.js'
|
||||
import VueTribute from 'vue-tribute'
|
||||
import he from 'he'
|
||||
import CurrentUserMixin from '../../mixins/currentUserMixin'
|
||||
import FocusOnCreate from '../../directives/focusOnCreate'
|
||||
import CurrentUserMixin from '../../mixins/currentUserMixin.js'
|
||||
import FocusOnCreate from '../../directives/focusOnCreate.js'
|
||||
import axios from '@nextcloud/axios'
|
||||
import ActorAvatar from '../ActorAvatar.vue'
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
import PreviewGrid from './PreviewGrid'
|
||||
import PreviewGrid from './PreviewGrid.vue'
|
||||
|
||||
export default {
|
||||
name: 'Composer',
|
||||
components: {
|
||||
PopoverMenu,
|
||||
Avatar,
|
||||
FileUpload,
|
||||
NcPopoverMenu,
|
||||
NcAvatar,
|
||||
NcEmojiPicker,
|
||||
NcButton,
|
||||
ActorAvatar,
|
||||
EmojiPicker,
|
||||
FileUpload,
|
||||
VueTribute,
|
||||
EmoticonOutline,
|
||||
Button,
|
||||
Send,
|
||||
Close,
|
||||
PreviewGrid,
|
||||
},
|
||||
directives: {
|
||||
|
@ -450,19 +458,20 @@ export default {
|
|||
let content = contentHtml.replace(/<(?!\/div)[^>]+>/gi, '').replace(/<\/div>/gi, '\n').trim()
|
||||
content = he.decode(content)
|
||||
|
||||
let data = {
|
||||
content: content,
|
||||
to: to,
|
||||
hashtags: hashtags,
|
||||
type: this.type,
|
||||
attachments: this.previewUrls.map(preview => preview.result), // TODO send the summary and other props too
|
||||
let formData = new FormData()
|
||||
formData.append('content', content)
|
||||
formData.append('to', to)
|
||||
formData.append('hashtags', hashtags)
|
||||
formData.append('type', this.type)
|
||||
for (const preview of this.previewUrls) {
|
||||
// TODO send the summary and other props too
|
||||
formData.append('attachments', preview.result)
|
||||
}
|
||||
|
||||
if (this.replyTo) {
|
||||
data.replyTo = this.replyTo.id
|
||||
formData.append('replyTo', this.replyTo.id)
|
||||
}
|
||||
|
||||
return data
|
||||
return formData
|
||||
},
|
||||
keyup(event) {
|
||||
if (event.shiftKey || event.ctrlKey) {
|
||||
|
@ -479,7 +488,7 @@ export default {
|
|||
|
||||
// Trick to validate last mention when the user directly clicks on the "post" button without validating it.
|
||||
let regex = /@([-\w]+)$/
|
||||
let lastMention = postData.content.match(regex)
|
||||
let lastMention = postData.get('content').match(regex)
|
||||
if (lastMention) {
|
||||
|
||||
// Ask the server for matching accounts, and wait for the results
|
||||
|
@ -487,13 +496,13 @@ export default {
|
|||
|
||||
// Validate the last mention only when it matches a single account
|
||||
if (result.data.result.accounts.length === 1) {
|
||||
postData.content = postData.content.replace(regex, '@' + result.data.result.accounts[0].account)
|
||||
postData.to.push(result.data.result.accounts[0].account)
|
||||
postData.set('content', postData.get('content').replace(regex, '@' + result.data.result.accounts[0].account))
|
||||
postData.set('to', postData.get('to').push(result.data.result.accounts[0].account))
|
||||
}
|
||||
}
|
||||
|
||||
// Abort if the post is a direct message and no valid mentions were found
|
||||
// if (this.type === 'direct' && postData.to.length === 0) {
|
||||
// if (this.type === 'direct' && postData.get('to').length === 0) {
|
||||
// OC.Notification.showTemporary(t('social', 'Error while trying to post your message: Could not find any valid recipients.'), { type: 'error' })
|
||||
// return
|
||||
// }
|
||||
|
@ -531,9 +540,9 @@ export default {
|
|||
padding: 10px;
|
||||
background-color: var(--color-main-background);
|
||||
position: sticky;
|
||||
top: 47px;
|
||||
z-index: 100;
|
||||
margin-bottom: 10px;
|
||||
top: 0;
|
||||
|
||||
&-form {
|
||||
flex-grow: 1;
|
||||
|
@ -566,21 +575,27 @@ export default {
|
|||
|
||||
.reply-to {
|
||||
background-image: url(../../../img/reply.svg);
|
||||
background-position: 5px 5px;
|
||||
background-position: 8px 12px;
|
||||
background-repeat: no-repeat;
|
||||
margin-left: 39px;
|
||||
margin-bottom: 20px;
|
||||
overflow: hidden;
|
||||
background-color: #fafafa;
|
||||
border-radius: 3px;
|
||||
background-color: var(--color-background-hover);
|
||||
border-radius: var(--border-radius-large);
|
||||
padding: 5px;
|
||||
padding-left: 30px;
|
||||
|
||||
.icon-close {
|
||||
display: inline-block;
|
||||
float: right;
|
||||
.reply-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.close-button {
|
||||
margin-left: auto;
|
||||
opacity: .7;
|
||||
padding: 3px;
|
||||
min-width: 30px;
|
||||
min-height: 30px;
|
||||
height: 30px;
|
||||
width: 30px !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -616,7 +631,6 @@ export default {
|
|||
align-items: flex-end;
|
||||
width: 100%;
|
||||
margin-top: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.emptySpace {
|
||||
|
|
|
@ -2,51 +2,51 @@
|
|||
<div class="preview-item-wrapper">
|
||||
<div class="preview-item" :style="backgroundStyle">
|
||||
<div class="preview-item__actions">
|
||||
<Button type="tertiary-no-background" @click="$emit('delete', index)">
|
||||
<NcButton type="tertiary-no-background" @click="$emit('delete', index)">
|
||||
<template #icon>
|
||||
<Close :size="16" fillColor="white" />
|
||||
</template>
|
||||
<span>{{ t('social', 'Delete') }}</span>
|
||||
</Button>
|
||||
<Button type="tertiary-no-background" @click="showModal">
|
||||
</NcButton>
|
||||
<NcButton type="tertiary-no-background" @click="showModal">
|
||||
<template #icon>
|
||||
<Edit :size="16" fillColor="white" />
|
||||
</template>
|
||||
<span>{{ t('social', 'Edit') }}</span>
|
||||
</Button>
|
||||
</NcButton>
|
||||
</div>
|
||||
|
||||
<div class="description-warning" v-if="preview.description.length === 0">
|
||||
{{ t('social', 'No description added') }}
|
||||
</div>
|
||||
|
||||
<Modal v-if="modal" @close="closeModal" size="small">
|
||||
<NcModal v-if="modal" @close="closeModal" size="small">
|
||||
<div class="modal__content">
|
||||
<label :for="`image-description-${index}`">
|
||||
{{ t('social', 'Describe for the visually impaired') }}
|
||||
</label>
|
||||
<textarea :id="`image-description-${index}`" v-model="preview.description">
|
||||
</textarea>
|
||||
<Button type="primary" @click="closeModal">{{ t('social', 'Close') }}</Button>
|
||||
<NcButton type="primary" @click="closeModal">{{ t('social', 'Close') }}</NcButton>
|
||||
</div>
|
||||
</Modal>
|
||||
</NcModal>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Close from 'vue-material-design-icons/Close'
|
||||
import Edit from 'vue-material-design-icons/Pencil'
|
||||
import Button from '@nextcloud/vue/dist/Components/Button'
|
||||
import Modal from '@nextcloud/vue/dist/Components/Modal'
|
||||
import Close from 'vue-material-design-icons/Close.vue'
|
||||
import Edit from 'vue-material-design-icons/Pencil.vue'
|
||||
import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
|
||||
import NcModal from '@nextcloud/vue/dist/Components/NcModal.js'
|
||||
|
||||
export default {
|
||||
name: 'PreviewGridItem',
|
||||
components: {
|
||||
Close,
|
||||
Edit,
|
||||
Button,
|
||||
Modal,
|
||||
NcButton,
|
||||
NcModal,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
@ -89,7 +89,7 @@ export default {
|
|||
}
|
||||
|
||||
.preview-item {
|
||||
border-radius: 4px;
|
||||
border-radius: var(--border-radius-large);
|
||||
background-color: #000;
|
||||
background-position: 50%;
|
||||
background-size: cover;
|
||||
|
|
|
@ -36,8 +36,8 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import accountMixins from '../mixins/accountMixins'
|
||||
import currentUser from '../mixins/currentUserMixin'
|
||||
import accountMixins from '../mixins/accountMixins.js'
|
||||
import currentUser from '../mixins/currentUserMixin.js'
|
||||
|
||||
export default {
|
||||
name: 'FollowButton',
|
||||
|
|
|
@ -3,26 +3,26 @@
|
|||
<div v-for="(item, index) in attachments" :key="index">
|
||||
<img :src="imageUrl(item)" @click="showModal(index)">
|
||||
</div>
|
||||
<modal v-show="modal" :has-previous="current > 0" :has-next="current < (attachments.length - 1)"
|
||||
<NcModal v-show="modal" :has-previous="current > 0" :has-next="current < (attachments.length - 1)"
|
||||
size="full" @close="closeModal" @previous="showPrevious"
|
||||
@next="showNext">
|
||||
<div class="modal__content">
|
||||
<canvas ref="modalCanvas" />
|
||||
</div>
|
||||
</modal>
|
||||
</NcModal>
|
||||
</masonry>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import serverData from '../mixins/serverData'
|
||||
import Modal from '@nextcloud/vue/dist/Components/Modal'
|
||||
import serverData from '../mixins/serverData.js'
|
||||
import NcModal from '@nextcloud/vue/dist/Components/NcModal.js'
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
|
||||
export default {
|
||||
name: 'PostAttachment',
|
||||
components: {
|
||||
Modal
|
||||
NcModal
|
||||
},
|
||||
mixins: [
|
||||
serverData
|
||||
|
|
|
@ -22,9 +22,9 @@
|
|||
|
||||
<template>
|
||||
<div v-if="profileAccount && accountInfo" class="user-profile">
|
||||
<avatar v-if="accountInfo.local" :user="localUid" :disable-tooltip="true"
|
||||
<NcAvatar v-if="accountInfo.local" :user="localUid" :disable-tooltip="true"
|
||||
:size="128" />
|
||||
<avatar v-else :url="avatarUrl" :disable-tooltip="true"
|
||||
<NcAvatar v-else :url="avatarUrl" :disable-tooltip="true"
|
||||
:size="128" />
|
||||
<h2>{{ displayName }}</h2>
|
||||
<!-- TODO: we have no details, timeline and follower list for non-local accounts for now -->
|
||||
|
@ -52,16 +52,17 @@
|
|||
</a>
|
||||
</p>
|
||||
<follow-button :account="accountInfo.account" :uid="uid" />
|
||||
<button v-if="serverData.public" class="primary" @click="followRemote">
|
||||
<NcButton v-if="serverData.public" class="primary" @click="followRemote">
|
||||
{{ t('social', 'Follow') }}
|
||||
</button>
|
||||
</NcButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.user-profile {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
flex-direction: column;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
|
@ -94,11 +95,12 @@
|
|||
}
|
||||
</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'
|
||||
import NcAvatar from '@nextcloud/vue/dist/Components/NcAvatar.js'
|
||||
import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
|
||||
import accountMixins from '../mixins/accountMixins.js'
|
||||
import serverData from '../mixins/serverData.js'
|
||||
import currentUser from '../mixins/currentUserMixin.js'
|
||||
import follow from '../mixins/follow.js'
|
||||
import FollowButton from './FollowButton.vue'
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
|
||||
|
@ -106,7 +108,8 @@ export default {
|
|||
name: 'ProfileInfo',
|
||||
components: {
|
||||
FollowButton,
|
||||
Avatar
|
||||
NcAvatar,
|
||||
NcButton,
|
||||
},
|
||||
mixins: [
|
||||
accountMixins,
|
||||
|
|
|
@ -1,25 +1,25 @@
|
|||
<template>
|
||||
<div v-if="item.actor_info" class="post-avatar">
|
||||
<Avatar v-if="item.local"
|
||||
<NcAvatar v-if="item.local"
|
||||
class="messages__avatar__icon"
|
||||
:show-user-status="false"
|
||||
menu-position="left"
|
||||
:user="userTest"
|
||||
:display-name="item.actor_info.account"
|
||||
:disable-tooltip="true" />
|
||||
<Avatar v-else
|
||||
<NcAvatar v-else
|
||||
:url="avatarUrl"
|
||||
:disable-tooltip="true" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Avatar from '@nextcloud/vue/dist/Components/Avatar'
|
||||
import NcAvatar from '@nextcloud/vue/dist/Components/NcAvatar.js'
|
||||
|
||||
export default {
|
||||
name: 'TimelineAvatar',
|
||||
components: {
|
||||
Avatar,
|
||||
NcAvatar,
|
||||
},
|
||||
props: {
|
||||
item: {
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
<span class="icon-boost" />
|
||||
</div>
|
||||
<div class="boost">
|
||||
<router-link v-if="item.actor_info" :to="{ name: 'profile', params: { account: item.local ? item.actor_info.preferredUsername : item.actor_info.account }}">
|
||||
<router-link v-if="!isProfilePage && 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>
|
||||
|
@ -50,7 +50,14 @@ export default {
|
|||
UserEntry
|
||||
},
|
||||
props: {
|
||||
item: { type: Object, default: () => {} }
|
||||
item: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
},
|
||||
isProfilePage: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
|
|
@ -58,7 +58,7 @@
|
|||
<script>
|
||||
import InfiniteLoading from 'vue-infinite-loading'
|
||||
import TimelineEntry from './TimelineEntry.vue'
|
||||
import CurrentUserMixin from './../mixins/currentUserMixin'
|
||||
import CurrentUserMixin from './../mixins/currentUserMixin.js'
|
||||
import EmptyContent from './EmptyContent.vue'
|
||||
import Logger from '../logger.js'
|
||||
|
||||
|
@ -73,7 +73,7 @@ export default {
|
|||
props: {
|
||||
type: { type: String, default: () => 'home' }
|
||||
},
|
||||
data: function() {
|
||||
data() {
|
||||
return {
|
||||
infoHidden: false,
|
||||
state: [],
|
||||
|
|
|
@ -25,51 +25,54 @@
|
|||
</div>
|
||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||
<div v-if="item.content" class="post-message">
|
||||
<MessageContent :source="source" />
|
||||
<RichText :text="source"
|
||||
:autolink="true"
|
||||
:reference-limit="2"
|
||||
:arguments="richParameters" />
|
||||
</div>
|
||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||
<div v-else class="post-message" v-html="item.actor_info.summary" />
|
||||
<div v-if="hasAttachments" class="post-attachments">
|
||||
<post-attachment :attachments="item.attachment" />
|
||||
</div>
|
||||
<div v-if="this.$route.params.type !== 'notifications' && !serverData.public" class="post-actions">
|
||||
<Button type="tertiary-no-background"
|
||||
<div v-if="this.$route && this.$route.params.type !== 'notifications' && !serverData.public" class="post-actions">
|
||||
<NcButton type="tertiary-no-background"
|
||||
v-tooltip="t('social', 'Reply')"
|
||||
@click="reply">
|
||||
<template #icon>
|
||||
<Reply :size="20" />
|
||||
</template>
|
||||
</Button>
|
||||
<Button type="tertiary-no-background"
|
||||
</NcButton>
|
||||
<NcButton type="tertiary-no-background"
|
||||
v-tooltip="t('social', 'Boost')"
|
||||
@click="boost">
|
||||
<template #icon>
|
||||
<Repeat :size="20" :fill-color="isBoosted ? 'blue' : 'black'" />
|
||||
<Repeat :size="20" :fill-color="isBoosted ? 'blue' : 'var(--color-main-text)'" />
|
||||
</template>
|
||||
</Button>
|
||||
<Button v-if="!isLiked"
|
||||
</NcButton>
|
||||
<NcButton v-if="!isLiked"
|
||||
type="tertiary-no-background"
|
||||
v-tooltip="t('social', 'Like')"
|
||||
@click="like">
|
||||
<template #icon>
|
||||
<HeartOutline :size="20" />
|
||||
</template>
|
||||
</Button>
|
||||
<Button v-if="isLiked"
|
||||
</NcButton>
|
||||
<NcButton v-if="isLiked"
|
||||
type="tertiary-no-background"
|
||||
v-tooltip="t('social', 'Undo Like')"
|
||||
@click="like">
|
||||
<template #icon>
|
||||
<Heart :size="20" :fill-color="'var(--color-error)'" />
|
||||
</template>
|
||||
</Button>
|
||||
<Actions>
|
||||
<ActionButton v-if="item.actor_info.account === cloudId"
|
||||
</NcButton>
|
||||
<NcActions>
|
||||
<NcActionButton v-if="item.actor_info.account === cloudId"
|
||||
@click="remove()"
|
||||
icon="icon-delete">
|
||||
{{ t('social', 'Delete') }}
|
||||
</ActionButton>
|
||||
</Actions>
|
||||
</NcActionButton>
|
||||
</NcActions>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -80,9 +83,9 @@ import pluginMention from 'linkifyjs/plugins/mention'
|
|||
import 'linkifyjs/string'
|
||||
import currentUser from './../mixins/currentUserMixin'
|
||||
import PostAttachment from './PostAttachment.vue'
|
||||
import Button from '@nextcloud/vue/dist/Components/Button'
|
||||
import Actions from '@nextcloud/vue/dist/Components/Actions'
|
||||
import ActionButton from '@nextcloud/vue/dist/Components/ActionButton'
|
||||
import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
|
||||
import NcActions from '@nextcloud/vue/dist/Components/NcActions.js'
|
||||
import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton.js'
|
||||
import Repeat from 'vue-material-design-icons/Repeat.vue'
|
||||
import Reply from 'vue-material-design-icons/Reply.vue'
|
||||
import Heart from 'vue-material-design-icons/Heart.vue'
|
||||
|
@ -91,6 +94,7 @@ import Logger from '../logger'
|
|||
import MessageContent from './MessageContent'
|
||||
import moment from '@nextcloud/moment'
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
import RichText from '@nextcloud/vue-richtext'
|
||||
|
||||
pluginMention(linkify)
|
||||
|
||||
|
@ -99,13 +103,14 @@ export default {
|
|||
components: {
|
||||
PostAttachment,
|
||||
MessageContent,
|
||||
Actions,
|
||||
ActionButton,
|
||||
Button,
|
||||
NcActions,
|
||||
NcActionButton,
|
||||
NcButton,
|
||||
Repeat,
|
||||
Reply,
|
||||
Heart,
|
||||
HeartOutline,
|
||||
RichText,
|
||||
},
|
||||
mixins: [currentUser],
|
||||
props: {
|
||||
|
@ -122,11 +127,10 @@ export default {
|
|||
source() {
|
||||
if (!this.item.source && this.item.content) {
|
||||
// local posts don't have a source json
|
||||
return {
|
||||
content: this.item.content,
|
||||
tag: []
|
||||
}
|
||||
console.debug(this.item.content)
|
||||
return this.item.content
|
||||
}
|
||||
console.debug(JSON.parse(this.item.source))
|
||||
return JSON.parse(this.item.source)
|
||||
},
|
||||
avatarUrl() {
|
||||
|
@ -146,7 +150,10 @@ export default {
|
|||
return false
|
||||
}
|
||||
return !!this.item.action.values.liked
|
||||
}
|
||||
},
|
||||
richParameters() {
|
||||
return {}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
|
@ -210,6 +217,7 @@ export default {
|
|||
}
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
@import '@nextcloud/vue-richtext/dist/style.css';
|
||||
.post-content {
|
||||
padding: 4px 4px 4px 8px;
|
||||
font-size: 15px;
|
||||
|
@ -217,6 +225,10 @@ export default {
|
|||
position: relative;
|
||||
width: 100%;
|
||||
|
||||
::v-deep a.widget-default {
|
||||
text-decoration: none !important;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-radius: 8px;
|
||||
background-color: var(--color-background-hover);
|
||||
|
|
|
@ -24,9 +24,9 @@
|
|||
<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"
|
||||
<NcAvatar v-if="item.local" :size="32" :user="item.preferredUsername"
|
||||
:disable-tooltip="true" />
|
||||
<avatar v-else :url="avatarUrl" />
|
||||
<NcAvatar v-else :url="avatarUrl" />
|
||||
</div>
|
||||
<div class="user-details">
|
||||
<router-link v-if="!serverData.public" :to="{ name: 'profile', params: { account: item.local ? item.preferredUsername : item.account }}">
|
||||
|
@ -55,9 +55,9 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import Avatar from '@nextcloud/vue/dist/Components/Avatar'
|
||||
import follow from '../mixins/follow'
|
||||
import currentUser from '../mixins/currentUserMixin'
|
||||
import NcAvatar from '@nextcloud/vue/dist/Components/NcAvatar.js'
|
||||
import follow from '../mixins/follow.js'
|
||||
import currentUser from '../mixins/currentUserMixin.js'
|
||||
import FollowButton from './FollowButton.vue'
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
|
||||
|
@ -65,7 +65,7 @@ export default {
|
|||
name: 'UserEntry',
|
||||
components: {
|
||||
FollowButton,
|
||||
Avatar
|
||||
NcAvatar,
|
||||
},
|
||||
mixins: [
|
||||
follow,
|
||||
|
|
|
@ -37,6 +37,9 @@ export default {
|
|||
* @property setup
|
||||
*/
|
||||
serverData() {
|
||||
if (!this.$store) {
|
||||
return {}
|
||||
}
|
||||
return this.$store.getters.getServerData
|
||||
},
|
||||
hostname() {
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
// SPDX-FileCopyrigthText: 2022 Carl Schwan <carl@carlschwan.eu>
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
// eslint-disable-next-line
|
||||
__webpack_nonce__ = btoa(OC.requestToken)
|
||||
// eslint-disable-next-line
|
||||
__webpack_public_path__ = OC.linkTo('social', 'js/')
|
||||
|
||||
import ProfilePageIntegration from './views/ProfilePageIntegration.vue'
|
||||
import Vue from 'vue'
|
||||
import { sync } from 'vuex-router-sync'
|
||||
|
||||
if (!OCA?.Core?.ProfileSections) {
|
||||
exit();
|
||||
}
|
||||
|
||||
Vue.prototype.t = t
|
||||
Vue.prototype.n = n
|
||||
Vue.prototype.OC = OC
|
||||
Vue.prototype.OCA = OCA
|
||||
|
||||
const View = Vue.extend(ProfilePageIntegration)
|
||||
|
||||
OCA.Core.ProfileSections.registerSection((el, userId) => {
|
||||
return View
|
||||
})
|
|
@ -144,17 +144,19 @@ const actions = {
|
|||
context.commit('setTimelineType', 'account')
|
||||
context.commit('setAccount', account)
|
||||
},
|
||||
post(context, post) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios.post(generateUrl('apps/social/api/v1/post'), { data: post }).then((response) => {
|
||||
Logger.info('Post created with token ' + response.data.result.token)
|
||||
resolve(response)
|
||||
}).catch((error) => {
|
||||
OC.Notification.showTemporary('Failed to create a post')
|
||||
Logger.error('Failed to create a post', { 'error': error.response })
|
||||
reject(error)
|
||||
async post(context, post) {
|
||||
try {
|
||||
const { data } = axios.post(generateUrl('apps/social/api/v1/post'), post, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
})
|
||||
})
|
||||
Logger.info('Post created with token ' + data.result.token)
|
||||
} catch (error) {
|
||||
OC.Notification.showTemporary('Failed to create a post')
|
||||
console.error(error)
|
||||
Logger.error('Failed to create a post', { 'error': error.response })
|
||||
}
|
||||
},
|
||||
postDelete(context, post) {
|
||||
return axios.delete(generateUrl(`apps/social/api/v1/post?id=${post.id}`)).then((response) => {
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
{{ t('social', 'Please confirm that you want to follow this account:') }}
|
||||
</p>
|
||||
|
||||
<avatar :url="avatarUrl" :disable-tooltip="true" :size="128" />
|
||||
<NcAvatar :url="avatarUrl" :disable-tooltip="true" :size="128" />
|
||||
<h2>{{ displayName }}</h2>
|
||||
<form v-if="!isFollowing" @submit.prevent="follow">
|
||||
<input type="submit" class="primary" value="Follow">
|
||||
|
@ -18,15 +18,15 @@
|
|||
</p>
|
||||
|
||||
<div v-if="isFollowing">
|
||||
<button @click="close">
|
||||
<NcButton @click="close">
|
||||
{{ t('social', 'Close') }}
|
||||
</button>
|
||||
</NcButton>
|
||||
</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" />
|
||||
<NcAvatar :user="serverData.local" :disable-tooltip="true" :size="128" />
|
||||
<h2>{{ displayName }}</h2>
|
||||
<form @submit.prevent="followRemote">
|
||||
<input v-model="remote" type="text" :placeholder="t('social', 'name@domain of your federation account')">
|
||||
|
@ -61,17 +61,17 @@
|
|||
</style>
|
||||
|
||||
<script>
|
||||
import Avatar from '@nextcloud/vue/dist/Components/Avatar'
|
||||
import NcAvatar from '@nextcloud/vue/dist/Components/NcAvatar.js'
|
||||
import axios from '@nextcloud/axios'
|
||||
import accountMixins from '../mixins/accountMixins'
|
||||
import currentuserMixin from '../mixins/currentUserMixin'
|
||||
import accountMixins from '../mixins/accountMixins.js'
|
||||
import currentuserMixin from '../mixins/currentUserMixin.js'
|
||||
import { loadState } from '@nextcloud/initial-state'
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
|
||||
export default {
|
||||
name: 'App',
|
||||
components: {
|
||||
Avatar
|
||||
NcAvatar,
|
||||
},
|
||||
mixins: [
|
||||
accountMixins,
|
||||
|
|
|
@ -25,7 +25,15 @@
|
|||
<profile-info v-if="accountLoaded && accountInfo" :uid="uid" />
|
||||
<!-- TODO: we have no details, timeline and follower list for non-local accounts for now -->
|
||||
<router-view v-if="accountLoaded && accountInfo && accountInfo.local" name="details" />
|
||||
<empty-content v-if="accountLoaded && !accountInfo" :item="emptyContentData" />
|
||||
<NcEmptyContent v-if="accountLoaded && !accountInfo"
|
||||
:title="t('social', 'User not found')"
|
||||
:description="t('social', 'Sorry, we could not find the account of {userId}', { userId: this.uid })">
|
||||
<template #icon>
|
||||
<img :src="emptyContentImage"
|
||||
class="icon-illustration"
|
||||
alt="">
|
||||
</template>
|
||||
</NcEmptyContent>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -39,14 +47,15 @@
|
|||
|
||||
<script>
|
||||
import ProfileInfo from './../components/ProfileInfo.vue'
|
||||
import EmptyContent from '../components/EmptyContent.vue'
|
||||
import accountMixins from '../mixins/accountMixins'
|
||||
import serverData from '../mixins/serverData'
|
||||
import NcEmptyContent from '@nextcloud/vue/dist/Components/NcEmptyContent.js'
|
||||
import accountMixins from '../mixins/accountMixins.js'
|
||||
import serverData from '../mixins/serverData.js'
|
||||
import { generateFilePath } from '@nextcloud/router'
|
||||
|
||||
export default {
|
||||
name: 'Profile',
|
||||
components: {
|
||||
EmptyContent,
|
||||
NcEmptyContent,
|
||||
ProfileInfo
|
||||
},
|
||||
mixins: [
|
||||
|
@ -63,13 +72,9 @@ export default {
|
|||
timeline: function() {
|
||||
return this.$store.getters.getTimeline
|
||||
},
|
||||
emptyContentData() {
|
||||
return {
|
||||
image: 'img/undraw/profile.svg',
|
||||
title: t('social', 'User not found'),
|
||||
description: t('social', 'Sorry, we could not find the account of {userId}', { userId: this.uid })
|
||||
}
|
||||
}
|
||||
emptyContentImage() {
|
||||
return generateFilePath('social', 'img', 'undraw/profile.svg')
|
||||
},
|
||||
},
|
||||
// Start fetching account information before mounting the component
|
||||
beforeMount() {
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
<template>
|
||||
<div>
|
||||
<h2>Social</h2>
|
||||
<transition-group name="list" tag="div">
|
||||
<TimelineEntry v-for="entry in timeline" :key="entry.id" :item="entry" :isProfilePage="true" />
|
||||
</transition-group>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ProfileInfo from './../components/ProfileInfo.vue'
|
||||
import TimelineEntry from './../components/TimelineEntry.vue'
|
||||
import axios from '@nextcloud/axios'
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
|
||||
export default {
|
||||
name: 'ProfilePageIntegration',
|
||||
props: {
|
||||
userId: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
accountInfo: null,
|
||||
timeline: [],
|
||||
}
|
||||
},
|
||||
components: {
|
||||
ProfileInfo,
|
||||
TimelineEntry,
|
||||
},
|
||||
computed: {
|
||||
getCount() {
|
||||
let account = this.accountInfo
|
||||
return (field) => account.details.count ? account.details.count[field] : ''
|
||||
},
|
||||
},
|
||||
// Start fetching account information before mounting the component
|
||||
beforeMount() {
|
||||
let fetchMethod = 'fetchPublicAccountInfo'
|
||||
|
||||
let uid = this.userId
|
||||
|
||||
axios.get(generateUrl(`apps/social/api/v1/account/${uid}/info`)).then((response) => {
|
||||
this.accountInfo = response.data.result.account
|
||||
console.log(this.accountInfo)
|
||||
})
|
||||
|
||||
const since = Math.floor(Date.now() / 1000) + 1
|
||||
|
||||
axios.get(generateUrl(`apps/social/api/v1/account/${uid}/stream?limit=25&since=${since}`)).then(({ data }) => {
|
||||
console.log(this.timeline)
|
||||
this.timeline = data.result
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -32,7 +32,6 @@
|
|||
<style scoped>
|
||||
|
||||
.social__welcome {
|
||||
max-width: 600px;
|
||||
margin: 15px auto;
|
||||
padding: 15px;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
|
@ -68,7 +67,6 @@
|
|||
}
|
||||
|
||||
.social__timeline {
|
||||
max-width: 600px;
|
||||
margin: 15px auto;
|
||||
}
|
||||
|
||||
|
@ -93,8 +91,8 @@
|
|||
|
||||
<script>
|
||||
import Composer from './../components/Composer/Composer.vue'
|
||||
import CurrentUserMixin from './../mixins/currentUserMixin'
|
||||
import follow from './../mixins/follow'
|
||||
import CurrentUserMixin from './../mixins/currentUserMixin.js'
|
||||
import follow from './../mixins/follow.js'
|
||||
import TimelineList from './../components/TimelineList.vue'
|
||||
|
||||
export default {
|
||||
|
|
|
@ -25,9 +25,9 @@ import Composer from '../components/Composer/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 currentUserMixin from '../mixins/currentUserMixin.js'
|
||||
import accountMixins from '../mixins/accountMixins.js'
|
||||
import serverData from '../mixins/serverData.js'
|
||||
import { loadState } from '@nextcloud/initial-state'
|
||||
|
||||
export default {
|
||||
|
|
|
@ -22,5 +22,4 @@
|
|||
*
|
||||
*/
|
||||
|
||||
script('social', 'social');
|
||||
style('social', 'style');
|
||||
\OCP\Util::addScript('social', 'social-social');
|
||||
|
|
|
@ -1,56 +1,13 @@
|
|||
// SPDX-FileCopyrigthText: 2022 Carl Schwan <carl@carlschwan.eu>
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
const path = require('path');
|
||||
const VueLoaderPlugin = require('vue-loader/lib/plugin');
|
||||
const webpackConfig = require('@nextcloud/webpack-vue-config')
|
||||
|
||||
webpackConfig.entry = {
|
||||
social: path.join(__dirname, 'src', 'main.js'),
|
||||
ostatus: path.join(__dirname, 'src', 'ostatus.js'),
|
||||
profilePage: path.join(__dirname, 'src', 'profile.js'),
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
entry: {
|
||||
social: path.join(__dirname, 'src', 'main.js'),
|
||||
ostatus: path.join(__dirname, 'src', 'ostatus.js'),
|
||||
},
|
||||
output: {
|
||||
path: path.resolve(__dirname, './js'),
|
||||
publicPath: '/js/',
|
||||
filename: '[name].js',
|
||||
chunkFilename: '[name].[chunkhash].js'
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.css$/,
|
||||
use: [
|
||||
'style-loader',
|
||||
'css-loader'
|
||||
]
|
||||
},
|
||||
{
|
||||
test: /\.scss$/,
|
||||
use: [
|
||||
'style-loader',
|
||||
'css-loader',
|
||||
'sass-loader'
|
||||
]
|
||||
},
|
||||
{
|
||||
test: /\.vue$/,
|
||||
loader: 'vue-loader'
|
||||
},
|
||||
{
|
||||
test: /\.js$/,
|
||||
loader: 'babel-loader',
|
||||
exclude: /node_modules/
|
||||
},
|
||||
{
|
||||
test: /\.(png|jpg|gif|svg)$/,
|
||||
loader: 'file-loader',
|
||||
options: {
|
||||
name: '[name].[ext]?[hash]'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
plugins: [new VueLoaderPlugin()],
|
||||
resolve: {
|
||||
extensions: ['*', '.js', '.vue'],
|
||||
symlinks: false
|
||||
}
|
||||
};
|
||||
module.exports = webpackConfig
|
||||
|
|
Ładowanie…
Reference in New Issue