kopia lustrzana https://github.com/nextcloud/social
Merge pull request #1615 from nextcloud/artonge/feat/adapt_to_new_timeline_api
Use new API for attachments postspull/1600/head
commit
164fbe2c3f
15
.babelrc.js
15
.babelrc.js
|
@ -1,14 +1,3 @@
|
|||
module.exports = {
|
||||
plugins: ['@babel/plugin-syntax-dynamic-import'],
|
||||
presets: [
|
||||
[
|
||||
'@babel/preset-env',
|
||||
{
|
||||
targets: {
|
||||
browsers: ['last 2 versions', 'ie >= 11']
|
||||
}
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
const babelConfig = require('@nextcloud/babel-config')
|
||||
|
||||
module.exports = babelConfig
|
||||
|
|
|
@ -1,24 +0,0 @@
|
|||
@include icon-black-white('reply', 'social', 1);
|
||||
@include icon-black-white('emoji', 'social', 1);
|
||||
@include icon-black-white('boost', 'social', 1);
|
||||
@include icon-black-white('upload', 'actions', 1, true);
|
||||
@include icon-black-white('notifications', 'social', 1);
|
||||
|
||||
.icon-boosted {
|
||||
@include icon-color('boost', 'social', '#0082c9', 1);
|
||||
}
|
||||
.icon-upload {
|
||||
@include icon-color('upload', 'actions', $color-black, 1, true);
|
||||
}
|
||||
|
||||
img.emoji {
|
||||
margin: 3px;
|
||||
width: 16px;
|
||||
vertical-align: text-bottom;
|
||||
}
|
||||
|
||||
|
||||
// quick fix - TODO - remove this and fix the left panel
|
||||
ul.app-navigation__list {
|
||||
display: none !important;
|
||||
}
|
|
@ -554,7 +554,7 @@ class ApiController extends Controller {
|
|||
int $limit = 20,
|
||||
int $max_id = 0,
|
||||
int $min_id = 0,
|
||||
int $since = 0
|
||||
int $since_id = 0
|
||||
): DataResponse {
|
||||
try {
|
||||
$this->initViewer(true);
|
||||
|
@ -568,7 +568,7 @@ class ApiController extends Controller {
|
|||
->setLimit($limit)
|
||||
->setMaxId($max_id)
|
||||
->setMinId($min_id)
|
||||
->setSince($since);
|
||||
->setSince($since_id);
|
||||
|
||||
$posts = $this->streamService->getTimeline($options);
|
||||
|
||||
|
|
|
@ -37,6 +37,7 @@ use Exception;
|
|||
use OCA\Social\AppInfo\Application;
|
||||
use OCA\Social\Exceptions\AccountDoesNotExistException;
|
||||
use OCA\Social\Exceptions\InvalidResourceException;
|
||||
use OCA\Social\Model\ActivityPub\ACore;
|
||||
use OCA\Social\Model\ActivityPub\Actor\Person;
|
||||
use OCA\Social\Model\ActivityPub\Object\Note;
|
||||
use OCA\Social\Model\ActivityPub\Stream;
|
||||
|
@ -537,8 +538,9 @@ class LocalController extends Controller {
|
|||
|
||||
$actor = $this->cacheActorService->getFromLocalAccount($username);
|
||||
$actor->setCompleteDetails(true);
|
||||
$actor->setExportFormat(ACore::FORMAT_LOCAL);
|
||||
|
||||
return $this->success(['account' => $actor]);
|
||||
return new DataResponse($actor, Http::STATUS_OK);
|
||||
} catch (Exception $e) {
|
||||
return $this->fail($e);
|
||||
}
|
||||
|
@ -588,8 +590,9 @@ class LocalController extends Controller {
|
|||
$this->initViewer();
|
||||
|
||||
$actor = $this->cacheActorService->getFromAccount($account);
|
||||
$actor->setExportFormat(ACore::FORMAT_LOCAL);
|
||||
|
||||
return $this->success(['account' => $actor]);
|
||||
return new DataResponse($actor, Http::STATUS_OK);
|
||||
} catch (Exception $e) {
|
||||
return $this->fail($e);
|
||||
}
|
||||
|
|
|
@ -451,7 +451,7 @@ class StreamRequest extends StreamRequestBuilder {
|
|||
* @return Stream[]
|
||||
*/
|
||||
private function getTimelineDirect(ProbeOptions $options): array {
|
||||
$qb = $this->getStreamSelectSql();
|
||||
$qb = $this->getStreamSelectSql($options->getFormat());
|
||||
|
||||
$qb->filterType(SocialAppNotification::TYPE);
|
||||
$qb->paginate($options);
|
||||
|
@ -476,7 +476,7 @@ class StreamRequest extends StreamRequestBuilder {
|
|||
* @return Stream[]
|
||||
*/
|
||||
private function getTimelineAccount(ProbeOptions $options): array {
|
||||
$qb = $this->getStreamSelectSql();
|
||||
$qb = $this->getStreamSelectSql($options->getFormat());
|
||||
|
||||
$qb->filterType(SocialAppNotification::TYPE);
|
||||
$qb->paginate($options);
|
||||
|
|
|
@ -592,7 +592,7 @@ class Stream extends ACore implements IQueryRow, JsonSerializable {
|
|||
public function exportAsNotification(): array {
|
||||
switch ($this->getSubType()) {
|
||||
case Like::TYPE:
|
||||
$type = 'favourites';
|
||||
$type = 'favourite';
|
||||
break;
|
||||
case Announce::TYPE:
|
||||
$type = 'mention';
|
||||
|
|
|
@ -19,6 +19,8 @@
|
|||
"@nextcloud/router": "^2.0.1",
|
||||
"@nextcloud/vue": "^7.4.0",
|
||||
"@nextcloud/vue-richtext": "^2.0.4",
|
||||
"blurhash": "^2.0.5",
|
||||
"debounce": "^1.2.1",
|
||||
"he": "^1.2.0",
|
||||
"linkify-plugin-mention": "^4.1.0",
|
||||
"linkify-string": "^4.1.0",
|
||||
|
@ -30,8 +32,8 @@
|
|||
"v-tooltip": "^4.0.0-beta.0",
|
||||
"vue": "^2.7.10",
|
||||
"vue-click-outside": "^1.0.7",
|
||||
"vue-contenteditable-directive": "^1.2.0",
|
||||
"vue-infinite-loading": "^2.4.4",
|
||||
"vue-masonry-css": "^1.0.3",
|
||||
"vue-material-design-icons": "^5.0.0",
|
||||
"vue-router": "^3.6.5",
|
||||
"vue-tribute": "^1.0.7",
|
||||
|
@ -47,6 +49,7 @@
|
|||
"@nextcloud/eslint-config": "^8.2.0",
|
||||
"@nextcloud/stylelint-config": "^2.3.0",
|
||||
"@nextcloud/webpack-vue-config": "^5.4.0",
|
||||
"copy-webpack-plugin": "^11.0.0",
|
||||
"cypress": "^11.2.0",
|
||||
"jest": "^29.3.1",
|
||||
"jest-serializer-vue": "^3.1.0",
|
||||
|
@ -3513,7 +3516,6 @@
|
|||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||
"integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@nodelib/fs.stat": "2.0.5",
|
||||
"run-parallel": "^1.1.9"
|
||||
|
@ -3527,7 +3529,6 @@
|
|||
"resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
|
||||
"integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
|
@ -3537,7 +3538,6 @@
|
|||
"resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
|
||||
"integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@nodelib/fs.scandir": "2.1.5",
|
||||
"fastq": "^1.6.0"
|
||||
|
@ -3796,8 +3796,7 @@
|
|||
"version": "7.0.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz",
|
||||
"integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==",
|
||||
"dev": true,
|
||||
"peer": true
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/json5": {
|
||||
"version": "0.0.29",
|
||||
|
@ -4798,7 +4797,6 @@
|
|||
"resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz",
|
||||
"integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"ajv": "^8.0.0"
|
||||
},
|
||||
|
@ -4816,7 +4814,6 @@
|
|||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz",
|
||||
"integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.1",
|
||||
"json-schema-traverse": "^1.0.0",
|
||||
|
@ -4832,8 +4829,7 @@
|
|||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
||||
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
|
||||
"dev": true,
|
||||
"peer": true
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/ajv-keywords": {
|
||||
"version": "3.5.2",
|
||||
|
@ -5585,6 +5581,11 @@
|
|||
"integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/blurhash": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/blurhash/-/blurhash-2.0.5.tgz",
|
||||
"integrity": "sha512-cRygWd7kGBQO3VEhPiTgq4Wc43ctsM+o46urrmPOiuAe+07fzlSB9OJVdpgDL0jPqXUVQ9ht7aq7kxOeJHRK+w=="
|
||||
},
|
||||
"node_modules/bn.js": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz",
|
||||
|
@ -6779,6 +6780,126 @@
|
|||
"dev": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/copy-webpack-plugin": {
|
||||
"version": "11.0.0",
|
||||
"resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-11.0.0.tgz",
|
||||
"integrity": "sha512-fX2MWpamkW0hZxMEg0+mYnA40LTosOSa5TqZ9GYIBzyJa9C3QUaMPSE2xAi/buNr8u89SfD9wHSQVBzrRa/SOQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"fast-glob": "^3.2.11",
|
||||
"glob-parent": "^6.0.1",
|
||||
"globby": "^13.1.1",
|
||||
"normalize-path": "^3.0.0",
|
||||
"schema-utils": "^4.0.0",
|
||||
"serialize-javascript": "^6.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14.15.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/webpack"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"webpack": "^5.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/copy-webpack-plugin/node_modules/ajv": {
|
||||
"version": "8.12.0",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz",
|
||||
"integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.1",
|
||||
"json-schema-traverse": "^1.0.0",
|
||||
"require-from-string": "^2.0.2",
|
||||
"uri-js": "^4.2.2"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/epoberezkin"
|
||||
}
|
||||
},
|
||||
"node_modules/copy-webpack-plugin/node_modules/ajv-keywords": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz",
|
||||
"integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"ajv": "^8.8.2"
|
||||
}
|
||||
},
|
||||
"node_modules/copy-webpack-plugin/node_modules/glob-parent": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
|
||||
"integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"is-glob": "^4.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/copy-webpack-plugin/node_modules/globby": {
|
||||
"version": "13.1.3",
|
||||
"resolved": "https://registry.npmjs.org/globby/-/globby-13.1.3.tgz",
|
||||
"integrity": "sha512-8krCNHXvlCgHDpegPzleMq07yMYTO2sXKASmZmquEYWEmCx6J5UTRbp5RwMJkTJGtcQ44YpiUYUiN0b9mzy8Bw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"dir-glob": "^3.0.1",
|
||||
"fast-glob": "^3.2.11",
|
||||
"ignore": "^5.2.0",
|
||||
"merge2": "^1.4.1",
|
||||
"slash": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/copy-webpack-plugin/node_modules/json-schema-traverse": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
||||
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/copy-webpack-plugin/node_modules/schema-utils": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.0.tgz",
|
||||
"integrity": "sha512-1edyXKgh6XnJsJSQ8mKWXnN/BVaIbFMLpouRUrXgVq7WYne5kw3MW7UPhO44uRXQSIpTSXoJbmrR2X0w9kUTyg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/json-schema": "^7.0.9",
|
||||
"ajv": "^8.8.0",
|
||||
"ajv-formats": "^2.1.1",
|
||||
"ajv-keywords": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12.13.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/webpack"
|
||||
}
|
||||
},
|
||||
"node_modules/copy-webpack-plugin/node_modules/slash": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz",
|
||||
"integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/core-js": {
|
||||
"version": "3.25.5",
|
||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.25.5.tgz",
|
||||
|
@ -7558,7 +7679,6 @@
|
|||
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
|
||||
"integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"path-type": "^4.0.0"
|
||||
},
|
||||
|
@ -9189,15 +9309,13 @@
|
|||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
||||
"dev": true,
|
||||
"peer": true
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/fast-glob": {
|
||||
"version": "3.2.11",
|
||||
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz",
|
||||
"integrity": "sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@nodelib/fs.stat": "^2.0.2",
|
||||
"@nodelib/fs.walk": "^1.2.3",
|
||||
|
@ -9258,7 +9376,6 @@
|
|||
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz",
|
||||
"integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"reusify": "^1.0.4"
|
||||
}
|
||||
|
@ -10219,7 +10336,6 @@
|
|||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz",
|
||||
"integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 4"
|
||||
}
|
||||
|
@ -13565,7 +13681,6 @@
|
|||
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
|
||||
"integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
|
@ -14425,7 +14540,6 @@
|
|||
"resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
|
||||
"integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
|
@ -14899,8 +15013,7 @@
|
|||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/quick-lru": {
|
||||
"version": "4.0.1",
|
||||
|
@ -15298,7 +15411,6 @@
|
|||
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
|
||||
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
|
@ -15405,7 +15517,6 @@
|
|||
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
|
||||
"integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"iojs": ">=1.0.0",
|
||||
"node": ">=0.10.0"
|
||||
|
@ -15461,7 +15572,6 @@
|
|||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"queue-microtask": "^1.2.2"
|
||||
}
|
||||
|
@ -15633,7 +15743,6 @@
|
|||
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz",
|
||||
"integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"randombytes": "^2.1.0"
|
||||
}
|
||||
|
@ -17410,7 +17519,6 @@
|
|||
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz",
|
||||
"integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"punycode": "^2.1.0"
|
||||
}
|
||||
|
@ -17582,11 +17690,6 @@
|
|||
"tinycolor2": "^1.1.2"
|
||||
}
|
||||
},
|
||||
"node_modules/vue-contenteditable-directive": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/vue-contenteditable-directive/-/vue-contenteditable-directive-1.2.0.tgz",
|
||||
"integrity": "sha512-9RuW1cboQBOUhURXiQpBD8XldyK2BYWhkWTnRw4Qmv8ZeQy+tGnnPs4XfemoPNf4KQW31Mx6UqEszlZYgoPeYw=="
|
||||
},
|
||||
"node_modules/vue-eslint-parser": {
|
||||
"version": "9.1.0",
|
||||
"resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.1.0.tgz",
|
||||
|
@ -17748,6 +17851,11 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/vue-masonry-css": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/vue-masonry-css/-/vue-masonry-css-1.0.3.tgz",
|
||||
"integrity": "sha512-viecHQiHVLez7HlYUQsv1wJb2MT/RDSzkDp6m3In41vPrk6OsBmT2qRE8LZqYIA4daIwrnx/Xm8h4fjOpuE3hw=="
|
||||
},
|
||||
"node_modules/vue-material-design-icons": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/vue-material-design-icons/-/vue-material-design-icons-5.1.2.tgz",
|
||||
|
@ -21213,7 +21321,6 @@
|
|||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||
"integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"requires": {
|
||||
"@nodelib/fs.stat": "2.0.5",
|
||||
"run-parallel": "^1.1.9"
|
||||
|
@ -21223,15 +21330,13 @@
|
|||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
|
||||
"integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
|
||||
"dev": true,
|
||||
"peer": true
|
||||
"dev": true
|
||||
},
|
||||
"@nodelib/fs.walk": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
|
||||
"integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"requires": {
|
||||
"@nodelib/fs.scandir": "2.1.5",
|
||||
"fastq": "^1.6.0"
|
||||
|
@ -21479,8 +21584,7 @@
|
|||
"version": "7.0.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz",
|
||||
"integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==",
|
||||
"dev": true,
|
||||
"peer": true
|
||||
"dev": true
|
||||
},
|
||||
"@types/json5": {
|
||||
"version": "0.0.29",
|
||||
|
@ -22279,7 +22383,6 @@
|
|||
"resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz",
|
||||
"integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"requires": {
|
||||
"ajv": "^8.0.0"
|
||||
},
|
||||
|
@ -22289,7 +22392,6 @@
|
|||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz",
|
||||
"integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"requires": {
|
||||
"fast-deep-equal": "^3.1.1",
|
||||
"json-schema-traverse": "^1.0.0",
|
||||
|
@ -22301,8 +22403,7 @@
|
|||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
||||
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
|
||||
"dev": true,
|
||||
"peer": true
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -22878,6 +22979,11 @@
|
|||
"integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==",
|
||||
"dev": true
|
||||
},
|
||||
"blurhash": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/blurhash/-/blurhash-2.0.5.tgz",
|
||||
"integrity": "sha512-cRygWd7kGBQO3VEhPiTgq4Wc43ctsM+o46urrmPOiuAe+07fzlSB9OJVdpgDL0jPqXUVQ9ht7aq7kxOeJHRK+w=="
|
||||
},
|
||||
"bn.js": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz",
|
||||
|
@ -23839,6 +23945,89 @@
|
|||
"dev": true,
|
||||
"peer": true
|
||||
},
|
||||
"copy-webpack-plugin": {
|
||||
"version": "11.0.0",
|
||||
"resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-11.0.0.tgz",
|
||||
"integrity": "sha512-fX2MWpamkW0hZxMEg0+mYnA40LTosOSa5TqZ9GYIBzyJa9C3QUaMPSE2xAi/buNr8u89SfD9wHSQVBzrRa/SOQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"fast-glob": "^3.2.11",
|
||||
"glob-parent": "^6.0.1",
|
||||
"globby": "^13.1.1",
|
||||
"normalize-path": "^3.0.0",
|
||||
"schema-utils": "^4.0.0",
|
||||
"serialize-javascript": "^6.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"ajv": {
|
||||
"version": "8.12.0",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz",
|
||||
"integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"fast-deep-equal": "^3.1.1",
|
||||
"json-schema-traverse": "^1.0.0",
|
||||
"require-from-string": "^2.0.2",
|
||||
"uri-js": "^4.2.2"
|
||||
}
|
||||
},
|
||||
"ajv-keywords": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz",
|
||||
"integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"fast-deep-equal": "^3.1.3"
|
||||
}
|
||||
},
|
||||
"glob-parent": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
|
||||
"integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"is-glob": "^4.0.3"
|
||||
}
|
||||
},
|
||||
"globby": {
|
||||
"version": "13.1.3",
|
||||
"resolved": "https://registry.npmjs.org/globby/-/globby-13.1.3.tgz",
|
||||
"integrity": "sha512-8krCNHXvlCgHDpegPzleMq07yMYTO2sXKASmZmquEYWEmCx6J5UTRbp5RwMJkTJGtcQ44YpiUYUiN0b9mzy8Bw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"dir-glob": "^3.0.1",
|
||||
"fast-glob": "^3.2.11",
|
||||
"ignore": "^5.2.0",
|
||||
"merge2": "^1.4.1",
|
||||
"slash": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"json-schema-traverse": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
||||
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
|
||||
"dev": true
|
||||
},
|
||||
"schema-utils": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.0.tgz",
|
||||
"integrity": "sha512-1edyXKgh6XnJsJSQ8mKWXnN/BVaIbFMLpouRUrXgVq7WYne5kw3MW7UPhO44uRXQSIpTSXoJbmrR2X0w9kUTyg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/json-schema": "^7.0.9",
|
||||
"ajv": "^8.8.0",
|
||||
"ajv-formats": "^2.1.1",
|
||||
"ajv-keywords": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"slash": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz",
|
||||
"integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"core-js": {
|
||||
"version": "3.25.5",
|
||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.25.5.tgz",
|
||||
|
@ -24440,7 +24629,6 @@
|
|||
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
|
||||
"integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"requires": {
|
||||
"path-type": "^4.0.0"
|
||||
}
|
||||
|
@ -25677,15 +25865,13 @@
|
|||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
||||
"dev": true,
|
||||
"peer": true
|
||||
"dev": true
|
||||
},
|
||||
"fast-glob": {
|
||||
"version": "3.2.11",
|
||||
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz",
|
||||
"integrity": "sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"requires": {
|
||||
"@nodelib/fs.stat": "^2.0.2",
|
||||
"@nodelib/fs.walk": "^1.2.3",
|
||||
|
@ -25733,7 +25919,6 @@
|
|||
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz",
|
||||
"integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"requires": {
|
||||
"reusify": "^1.0.4"
|
||||
}
|
||||
|
@ -26452,8 +26637,7 @@
|
|||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz",
|
||||
"integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==",
|
||||
"dev": true,
|
||||
"peer": true
|
||||
"dev": true
|
||||
},
|
||||
"immutable": {
|
||||
"version": "4.1.0",
|
||||
|
@ -28954,8 +29138,7 @@
|
|||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
|
||||
"integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
|
||||
"dev": true,
|
||||
"peer": true
|
||||
"dev": true
|
||||
},
|
||||
"methods": {
|
||||
"version": "1.1.2",
|
||||
|
@ -29617,8 +29800,7 @@
|
|||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
|
||||
"integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
|
||||
"dev": true,
|
||||
"peer": true
|
||||
"dev": true
|
||||
},
|
||||
"pbkdf2": {
|
||||
"version": "3.1.2",
|
||||
|
@ -29964,8 +30146,7 @@
|
|||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
||||
"integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
|
||||
"dev": true,
|
||||
"peer": true
|
||||
"dev": true
|
||||
},
|
||||
"quick-lru": {
|
||||
"version": "4.0.1",
|
||||
|
@ -30288,8 +30469,7 @@
|
|||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
|
||||
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
|
||||
"dev": true,
|
||||
"peer": true
|
||||
"dev": true
|
||||
},
|
||||
"requireindex": {
|
||||
"version": "1.2.0",
|
||||
|
@ -30367,8 +30547,7 @@
|
|||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
|
||||
"integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
|
||||
"dev": true,
|
||||
"peer": true
|
||||
"dev": true
|
||||
},
|
||||
"rfdc": {
|
||||
"version": "1.3.0",
|
||||
|
@ -30400,7 +30579,6 @@
|
|||
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
|
||||
"integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"requires": {
|
||||
"queue-microtask": "^1.2.2"
|
||||
}
|
||||
|
@ -30526,7 +30704,6 @@
|
|||
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz",
|
||||
"integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"requires": {
|
||||
"randombytes": "^2.1.0"
|
||||
}
|
||||
|
@ -31912,7 +32089,6 @@
|
|||
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz",
|
||||
"integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"requires": {
|
||||
"punycode": "^2.1.0"
|
||||
}
|
||||
|
@ -32067,11 +32243,6 @@
|
|||
"tinycolor2": "^1.1.2"
|
||||
}
|
||||
},
|
||||
"vue-contenteditable-directive": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/vue-contenteditable-directive/-/vue-contenteditable-directive-1.2.0.tgz",
|
||||
"integrity": "sha512-9RuW1cboQBOUhURXiQpBD8XldyK2BYWhkWTnRw4Qmv8ZeQy+tGnnPs4XfemoPNf4KQW31Mx6UqEszlZYgoPeYw=="
|
||||
},
|
||||
"vue-eslint-parser": {
|
||||
"version": "9.1.0",
|
||||
"resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.1.0.tgz",
|
||||
|
@ -32186,6 +32357,11 @@
|
|||
"vue-style-loader": "^4.1.0"
|
||||
}
|
||||
},
|
||||
"vue-masonry-css": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/vue-masonry-css/-/vue-masonry-css-1.0.3.tgz",
|
||||
"integrity": "sha512-viecHQiHVLez7HlYUQsv1wJb2MT/RDSzkDp6m3In41vPrk6OsBmT2qRE8LZqYIA4daIwrnx/Xm8h4fjOpuE3hw=="
|
||||
},
|
||||
"vue-material-design-icons": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/vue-material-design-icons/-/vue-material-design-icons-5.1.2.tgz",
|
||||
|
|
|
@ -42,6 +42,8 @@
|
|||
"@nextcloud/router": "^2.0.1",
|
||||
"@nextcloud/vue": "^7.4.0",
|
||||
"@nextcloud/vue-richtext": "^2.0.4",
|
||||
"blurhash": "^2.0.5",
|
||||
"debounce": "^1.2.1",
|
||||
"he": "^1.2.0",
|
||||
"linkify-plugin-mention": "^4.1.0",
|
||||
"linkify-string": "^4.1.0",
|
||||
|
@ -53,8 +55,8 @@
|
|||
"v-tooltip": "^4.0.0-beta.0",
|
||||
"vue": "^2.7.10",
|
||||
"vue-click-outside": "^1.0.7",
|
||||
"vue-contenteditable-directive": "^1.2.0",
|
||||
"vue-infinite-loading": "^2.4.4",
|
||||
"vue-masonry-css": "^1.0.3",
|
||||
"vue-material-design-icons": "^5.0.0",
|
||||
"vue-router": "^3.6.5",
|
||||
"vue-tribute": "^1.0.7",
|
||||
|
@ -80,6 +82,7 @@
|
|||
"@nextcloud/eslint-config": "^8.2.0",
|
||||
"@nextcloud/stylelint-config": "^2.3.0",
|
||||
"@nextcloud/webpack-vue-config": "^5.4.0",
|
||||
"copy-webpack-plugin": "^11.0.0",
|
||||
"cypress": "^11.2.0",
|
||||
"jest": "^29.3.1",
|
||||
"jest-serializer-vue": "^3.1.0",
|
||||
|
|
11
src/App.vue
11
src/App.vue
|
@ -106,9 +106,11 @@ export default {
|
|||
}
|
||||
},
|
||||
computed: {
|
||||
/** @return {import('vue').PropType<import('../types/Mastodon.js').Account>} */
|
||||
timeline() {
|
||||
return this.$store.getters.getTimeline
|
||||
},
|
||||
/** @return {{items: {id: string, icon: object, title: string, to: { name: string } }, loading: boolean}} */
|
||||
menu() {
|
||||
const defaultCategories = [
|
||||
{
|
||||
|
@ -152,7 +154,7 @@ export default {
|
|||
title: t('social', 'Liked'),
|
||||
to: {
|
||||
name: 'timeline',
|
||||
params: { type: 'liked' },
|
||||
params: { type: 'favourites' },
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -263,3 +265,10 @@ export default {
|
|||
}
|
||||
|
||||
</style>
|
||||
<style lang="css">
|
||||
img.emoji {
|
||||
margin: 3px;
|
||||
width: 16px;
|
||||
vertical-align: text-bottom;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -21,10 +21,10 @@
|
|||
-->
|
||||
|
||||
<template>
|
||||
<NcAvatar v-if="actor.local"
|
||||
<NcAvatar v-if="isLocal"
|
||||
:size="size"
|
||||
:user="actor.preferredUsername"
|
||||
:display-name="actor.account"
|
||||
:user="actor.username"
|
||||
:display-name="actor.acct"
|
||||
:disable-tooltip="true"
|
||||
:show-user-status="false" />
|
||||
<NcAvatar v-else
|
||||
|
@ -44,8 +44,15 @@ export default {
|
|||
NcAvatar,
|
||||
},
|
||||
props: {
|
||||
actor: { type: Object, default: () => {} },
|
||||
size: { type: Number, default: 32 },
|
||||
/** @type {import('vue').PropType<import('../types/Mastodon.js').Account>} */
|
||||
actor: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
size: {
|
||||
type: Number,
|
||||
default: 32,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
@ -53,9 +60,16 @@ export default {
|
|||
}
|
||||
},
|
||||
computed: {
|
||||
/** @return {string} */
|
||||
avatarUrl() {
|
||||
return generateUrl('/apps/social/api/v1/global/actor/avatar?id=' + this.item.attributedTo)
|
||||
},
|
||||
/**
|
||||
* @return {boolean}
|
||||
*/
|
||||
isLocal() {
|
||||
return !this.actor.acct.includes('@')
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -26,6 +26,8 @@
|
|||
<input id="file-upload"
|
||||
ref="fileUploadInput"
|
||||
type="file"
|
||||
accept="image/*"
|
||||
multiple="true"
|
||||
tabindex="-1"
|
||||
aria-hidden="true"
|
||||
class="hidden-visually"
|
||||
|
@ -47,8 +49,8 @@
|
|||
<div v-if="replyTo" class="reply-to">
|
||||
<p class="reply-info">
|
||||
<span>{{ t('social', 'In reply to') }}</span>
|
||||
<ActorAvatar :actor="replyTo.actor_info" :size="16" />
|
||||
<strong>{{ replyTo.actor_info.account }}</strong>
|
||||
<ActorAvatar :actor="replyTo.account" :size="16" />
|
||||
<strong>{{ replyTo.account.acct }}</strong>
|
||||
<NcButton type="tertiary"
|
||||
class="close-button"
|
||||
:aria-label="t('social', 'Close reply')"
|
||||
|
@ -64,25 +66,24 @@
|
|||
</div>
|
||||
<form class="new-post-form" @submit.prevent="createPost">
|
||||
<VueTribute :options="tributeOptions">
|
||||
<!-- eslint-disable-next-line vue/valid-v-model -->
|
||||
<div ref="composerInput"
|
||||
v-contenteditable:post.dangerousHTML="canType && !loading"
|
||||
:disabled="loading"
|
||||
class="message"
|
||||
placeholder="What would you like to share?"
|
||||
:class="{'icon-loading': loading}"
|
||||
@keyup.prevent.enter="keyup"
|
||||
@input="updateStatusContent"
|
||||
@tribute-replaced="updatePostFromTribute" />
|
||||
</VueTribute>
|
||||
|
||||
<PreviewGrid :uploading="false"
|
||||
:upload-progress="0.4"
|
||||
:miniatures="previewUrls"
|
||||
:miniatures="attachments"
|
||||
@deleted="deletePreview" />
|
||||
|
||||
<div class="options">
|
||||
<NcButton v-tooltip="t('social', 'Add attachment')"
|
||||
type="tertiary"
|
||||
:disabled="previewUrls.length >= 1"
|
||||
:aria-label="t('social', 'Add attachment')"
|
||||
@click.prevent="clickImportInput">
|
||||
<template #icon>
|
||||
|
@ -94,7 +95,7 @@
|
|||
<NcEmojiPicker ref="emojiPicker"
|
||||
:search="search"
|
||||
:close-on-select="false"
|
||||
:container="container"
|
||||
container="#content-vue"
|
||||
@select="insert">
|
||||
<NcButton v-tooltip="t('social', 'Add emoji')"
|
||||
type="tertiary"
|
||||
|
@ -107,18 +108,11 @@
|
|||
</NcEmojiPicker>
|
||||
</div>
|
||||
|
||||
<div v-click-outside="hidePopoverMenu" class="popovermenu-parent">
|
||||
<NcButton v-tooltip="t('social', 'Visibility')"
|
||||
type="tertiary"
|
||||
:class="currentVisibilityIconClass"
|
||||
@click.prevent="togglePopoverMenu" />
|
||||
<div :class="{open: menuOpened}" class="popovermenu">
|
||||
<NcPopoverMenu :menu="visibilityPopover" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<VisibilitySelect :type.sync="type" />
|
||||
<div class="emptySpace" />
|
||||
<NcButton :value="currentVisibilityPostLabel"
|
||||
<SubmitStatusButton :type="type" :disabled="canPost || loading" @click="createPost" />
|
||||
|
||||
<!-- <NcButton :value="currentVisibilityPostLabel"
|
||||
:disabled="!canPost"
|
||||
native-type="submit"
|
||||
type="primary"
|
||||
|
@ -127,7 +121,7 @@
|
|||
<Send title="" :size="22" decorative />
|
||||
</template>
|
||||
{{ postTo }}
|
||||
</NcButton>
|
||||
</NcButton> -->
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
@ -136,12 +130,11 @@
|
|||
<script>
|
||||
|
||||
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 debounce from 'debounce'
|
||||
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'
|
||||
|
@ -151,11 +144,18 @@ import axios from '@nextcloud/axios'
|
|||
import ActorAvatar from '../ActorAvatar.vue'
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
import PreviewGrid from './PreviewGrid.vue'
|
||||
import VisibilitySelect from './VisibilitySelect.vue'
|
||||
import SubmitStatusButton from './SubmitStatusButton.vue'
|
||||
|
||||
/**
|
||||
* @typedef LocalAttachment
|
||||
* @property {File} file - The file object from the input element.
|
||||
* @property {import('../../types/Mastodon.js').MediaAttachment} data - The attachment information from the server.
|
||||
*/
|
||||
|
||||
export default {
|
||||
name: 'Composer',
|
||||
components: {
|
||||
NcPopoverMenu,
|
||||
NcAvatar,
|
||||
NcEmojiPicker,
|
||||
NcButton,
|
||||
|
@ -163,25 +163,24 @@ export default {
|
|||
FileUpload,
|
||||
VueTribute,
|
||||
EmoticonOutline,
|
||||
Send,
|
||||
Close,
|
||||
PreviewGrid,
|
||||
VisibilitySelect,
|
||||
SubmitStatusButton,
|
||||
},
|
||||
directives: {
|
||||
FocusOnCreate,
|
||||
},
|
||||
mixins: [CurrentUserMixin],
|
||||
props: {},
|
||||
data() {
|
||||
return {
|
||||
statusContent: '',
|
||||
type: localStorage.getItem('social.lastPostType') || 'followers',
|
||||
loading: false,
|
||||
post: '',
|
||||
miniatures: [], // miniatures of images stored in postAttachments
|
||||
postAttachments: [], // The toot's attachments
|
||||
previewUrls: [],
|
||||
canType: true,
|
||||
/** @type {Object<string, LocalAttachment>} */
|
||||
attachments: {},
|
||||
search: '',
|
||||
/** @type {import('../../types/Mastodon.js').Status} */
|
||||
replyTo: null,
|
||||
tributeOptions: {
|
||||
spaceSelectsMatch: true,
|
||||
|
@ -201,25 +200,23 @@ export default {
|
|||
return '<span class="mention" contenteditable="false">'
|
||||
+ '<a href="' + item.original.url + '" target="_blank"><img src="' + item.original.avatar + '" />@' + item.original.value + '</a></span>'
|
||||
},
|
||||
values: (text, cb) => {
|
||||
const users = []
|
||||
|
||||
values: debounce(async (text, populate) => {
|
||||
if (text.length < 1) {
|
||||
cb(users)
|
||||
populate([])
|
||||
}
|
||||
this.remoteSearchAccounts(text).then((result) => {
|
||||
for (const i in result.data.result.accounts) {
|
||||
const user = result.data.result.accounts[i]
|
||||
users.push({
|
||||
key: user.preferredUsername,
|
||||
value: user.account,
|
||||
url: user.url,
|
||||
avatar: user.local ? generateUrl(`/avatar/${user.preferredUsername}/32`) : generateUrl(`apps/social/api/v1/global/actor/avatar?id=${user.id}`),
|
||||
})
|
||||
}
|
||||
cb(users)
|
||||
})
|
||||
},
|
||||
|
||||
const response = await this.remoteSearchAccounts(text)
|
||||
|
||||
const users = response.data.result.accounts.map((user) => ({
|
||||
key: user.preferredUsername,
|
||||
value: user.account,
|
||||
url: user.url,
|
||||
avatar: user.local ? generateUrl(`/avatar/${user.preferredUsername}/32`) : generateUrl(`apps/social/api/v1/global/actor/avatar?id=${user.id}`),
|
||||
}))
|
||||
|
||||
console.debug('[Composer] Found users for', text, response.data.result, users)
|
||||
populate(users)
|
||||
}, 200),
|
||||
},
|
||||
{
|
||||
trigger: '#',
|
||||
|
@ -237,29 +234,20 @@ export default {
|
|||
return '<span class="hashtag" contenteditable="false">'
|
||||
+ '<a href="' + generateUrl('/timeline/tags/' + tag) + '" target="_blank">#' + tag + '</a></span>'
|
||||
},
|
||||
values: (text, cb) => {
|
||||
const tags = []
|
||||
|
||||
values: debounce(async (text, populate) => {
|
||||
if (text.length < 1) {
|
||||
cb(tags)
|
||||
populate([])
|
||||
}
|
||||
this.remoteSearchHashtags(text).then((result) => {
|
||||
if (result.data.result.exact) {
|
||||
tags.push({
|
||||
key: result.data.result.exact,
|
||||
value: result.data.result.exact,
|
||||
})
|
||||
}
|
||||
for (const i in result.data.result.tags) {
|
||||
const tag = result.data.result.tags[i]
|
||||
tags.push({
|
||||
key: tag.hashtag,
|
||||
value: tag.hashtag,
|
||||
})
|
||||
}
|
||||
cb(tags)
|
||||
})
|
||||
},
|
||||
|
||||
const response = await this.remoteSearchHashtags(text)
|
||||
const tags = [
|
||||
...(response.data.result.exact && !Array.isArray(response.data.result.exact) ? [{ key: response.data.result.exact, value: response.data.result.exact }] : []),
|
||||
...response.data.result.tags.map(({ hashtag }) => ({ key: hashtag, value: hashtag })),
|
||||
]
|
||||
|
||||
console.debug('[Composer] Found tags for', text, response.data.result, tags)
|
||||
populate(tags)
|
||||
}, 200),
|
||||
},
|
||||
],
|
||||
noMatchTemplate() {
|
||||
|
@ -272,123 +260,15 @@ export default {
|
|||
}
|
||||
},
|
||||
},
|
||||
menuOpened: false,
|
||||
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
postTo() {
|
||||
switch (this.type) {
|
||||
case 'public':
|
||||
case 'unlisted':
|
||||
return t('social', 'Post')
|
||||
case 'followers':
|
||||
return t('social', 'Post to followers')
|
||||
case 'direct':
|
||||
return t('social', 'Post to mentioned users')
|
||||
}
|
||||
return ''
|
||||
},
|
||||
currentVisibilityIconClass() {
|
||||
return this.visibilityIconClass(this.type)
|
||||
},
|
||||
visibilityIconClass() {
|
||||
return (type) => {
|
||||
if (typeof type === 'undefined') {
|
||||
type = this.type
|
||||
}
|
||||
switch (type) {
|
||||
case 'public':
|
||||
return 'icon-link'
|
||||
case 'followers':
|
||||
return 'icon-contacts-dark'
|
||||
case 'direct':
|
||||
return 'icon-external'
|
||||
case 'unlisted':
|
||||
return 'icon-password'
|
||||
}
|
||||
}
|
||||
},
|
||||
currentVisibilityPostLabel() {
|
||||
return this.visibilityPostLabel(this.type)
|
||||
},
|
||||
visibilityPostLabel() {
|
||||
return (type) => {
|
||||
if (typeof type === 'undefined') {
|
||||
type = this.type
|
||||
}
|
||||
switch (type) {
|
||||
case 'public':
|
||||
return t('social', 'Post publicly')
|
||||
case 'followers':
|
||||
return t('social', 'Post to followers')
|
||||
case 'direct':
|
||||
return t('social', 'Post to recipients')
|
||||
case 'unlisted':
|
||||
return t('social', 'Post unlisted')
|
||||
}
|
||||
}
|
||||
},
|
||||
activeState() {
|
||||
return (type) => {
|
||||
if (type === this.type) {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
},
|
||||
visibilityPopover() {
|
||||
return [
|
||||
{
|
||||
action: () => {
|
||||
this.switchType('public')
|
||||
},
|
||||
icon: this.visibilityIconClass('public'),
|
||||
active: this.activeState('public'),
|
||||
text: t('social', 'Public'),
|
||||
longtext: t('social', 'Post to public timelines'),
|
||||
},
|
||||
{
|
||||
action: () => {
|
||||
this.switchType('unlisted')
|
||||
},
|
||||
icon: this.visibilityIconClass('unlisted'),
|
||||
active: this.activeState('unlisted'),
|
||||
text: t('social', 'Unlisted'),
|
||||
longtext: t('social', 'Do not post to public timelines'),
|
||||
},
|
||||
{
|
||||
action: () => {
|
||||
this.switchType('followers')
|
||||
},
|
||||
icon: this.visibilityIconClass('followers'),
|
||||
active: this.activeState('followers'),
|
||||
text: t('social', 'Followers'),
|
||||
longtext: t('social', 'Post to followers only'),
|
||||
},
|
||||
{
|
||||
action: () => {
|
||||
this.switchType('direct')
|
||||
},
|
||||
icon: this.visibilityIconClass('direct'),
|
||||
active: this.activeState('direct'),
|
||||
text: t('social', 'Direct'),
|
||||
longtext: t('social', 'Post to mentioned users only'),
|
||||
},
|
||||
]
|
||||
},
|
||||
container() {
|
||||
return '#content-vue'
|
||||
},
|
||||
containerElement() {
|
||||
return document.querySelector(this.container)
|
||||
},
|
||||
/** @return {boolean} */
|
||||
canPost() {
|
||||
if (this.previewUrls.length > 0) {
|
||||
if (Object.keys(this.attachments).length > 0) {
|
||||
return true
|
||||
}
|
||||
return this.post.length !== 0 && this.post !== '<br>'
|
||||
return this.statusContent.length !== 0 && this.statusContent !== '<br>'
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
|
@ -398,95 +278,63 @@ export default {
|
|||
})
|
||||
},
|
||||
methods: {
|
||||
updateStatusContent() {
|
||||
this.statusContent = this.$refs.composerInput.innerHTML
|
||||
},
|
||||
clickImportInput() {
|
||||
this.$refs.fileUploadInput.click()
|
||||
},
|
||||
/** @param {InputEvent} event */
|
||||
handleFileChange(event) {
|
||||
event.target.files.forEach((file) => {
|
||||
this.previewUrls.push({
|
||||
description: '',
|
||||
url: URL.createObjectURL(file),
|
||||
result: file,
|
||||
/** @type {HTMLInputElement} */
|
||||
const target = event.target
|
||||
Array.from(target.files).forEach(async (file) => {
|
||||
const url = URL.createObjectURL(file)
|
||||
this.$set(this.attachments, url, {
|
||||
file,
|
||||
data: null,
|
||||
})
|
||||
this.$set(this.attachments[url], 'data', await this.$store.dispatch('createMedia', file))
|
||||
})
|
||||
},
|
||||
removeAttachment(idx) {
|
||||
this.previewUrls.splice(idx, 1)
|
||||
},
|
||||
insert(emoji) {
|
||||
console.debug('[Composer] insert emoji', emoji)
|
||||
if (typeof emoji === 'object') {
|
||||
const category = Object.keys(emoji)[0]
|
||||
const emojis = emoji[category]
|
||||
const firstEmoji = Object.keys(emojis)[0]
|
||||
emoji = emojis[firstEmoji]
|
||||
}
|
||||
this.post += this.$twemoji.parse(emoji) + ' '
|
||||
this.$refs.composerInput.innerHTML += this.$twemoji.parse(emoji) + ' '
|
||||
},
|
||||
togglePopoverMenu() {
|
||||
this.menuOpened = !this.menuOpened
|
||||
},
|
||||
hidePopoverMenu() {
|
||||
this.menuOpened = false
|
||||
},
|
||||
switchType(type) {
|
||||
this.type = type
|
||||
this.menuOpened = false
|
||||
localStorage.setItem('social.lastPostType', type)
|
||||
},
|
||||
getPostData() {
|
||||
const element = this.$refs.composerInput.cloneNode(true)
|
||||
Array.from(element.getElementsByClassName('emoji')).forEach((emoji) => {
|
||||
const em = document.createTextNode(emoji.getAttribute('alt'))
|
||||
emoji.replaceWith(em)
|
||||
})
|
||||
|
||||
const contentHtml = element.innerHTML
|
||||
/** @type {Element} */
|
||||
const lastChild = this.$refs.composerInput.lastChild
|
||||
const div = document.createElement('div')
|
||||
div.innerHTML = this.$twemoji.parse(emoji) + ' '
|
||||
|
||||
// Extract mentions from content and create an array out of them
|
||||
const to = []
|
||||
const mentionRegex = /<span class="mention"[^>]+><a[^>]+><img[^>]+>@([\w-_.]+@[\w-.]+)/g
|
||||
let match = null
|
||||
do {
|
||||
match = mentionRegex.exec(contentHtml)
|
||||
if (match) {
|
||||
to.push(match[1])
|
||||
if (lastChild === null) {
|
||||
this.$refs.composerInput.innerHTML = div.innerHTML
|
||||
} else {
|
||||
|
||||
// Content usually ends with </br> or </>
|
||||
// This makes sure that we put the emoji before those tags.
|
||||
switch (lastChild.tagName) {
|
||||
case 'BR':
|
||||
lastChild.before(div.firstChild)
|
||||
break
|
||||
case 'DIV':
|
||||
switch (lastChild.lastChild.tagName) {
|
||||
case 'BR':
|
||||
lastChild.lastChild.before(div.firstChild)
|
||||
break
|
||||
default:
|
||||
lastChild.append(div.firstChild)
|
||||
}
|
||||
break
|
||||
default:
|
||||
lastChild.after(div.firstChild)
|
||||
}
|
||||
} while (match)
|
||||
|
||||
// Add author of original post in case of reply
|
||||
if (this.replyTo !== null) {
|
||||
to.push(this.replyTo.actor_info.account)
|
||||
}
|
||||
|
||||
// Extract hashtags from content and create an array ot of them
|
||||
const hashtagRegex = />#([^<]+)</g
|
||||
const hashtags = []
|
||||
match = null
|
||||
do {
|
||||
match = hashtagRegex.exec(contentHtml)
|
||||
if (match) {
|
||||
hashtags.push(match[1])
|
||||
}
|
||||
} while (match)
|
||||
|
||||
// Remove all html tags but </div> (wich we turn in newlines) and decode the remaining html entities
|
||||
let content = contentHtml.replace(/<(?!\/div)[^>]+>/gi, '').replace(/<\/div>/gi, '\n').trim()
|
||||
content = he.decode(content)
|
||||
|
||||
const formData = new FormData()
|
||||
formData.append('content', content)
|
||||
to.forEach(to => formData.append('to[]', to))
|
||||
hashtags.forEach(hashtag => formData.append('hashtags[]', hashtag))
|
||||
formData.append('type', this.type)
|
||||
this.previewUrls.forEach(preview => formData.append('attachments[]', preview.result))
|
||||
this.previewUrls.forEach(preview => formData.append('attachmentDescriptions[]', preview.description))
|
||||
|
||||
if (this.replyTo) {
|
||||
formData.append('replyTo', this.replyTo.id)
|
||||
}
|
||||
|
||||
return formData
|
||||
this.updateStatusContent()
|
||||
},
|
||||
keyup(event) {
|
||||
if (event.shiftKey || event.ctrlKey) {
|
||||
|
@ -494,45 +342,44 @@ export default {
|
|||
}
|
||||
},
|
||||
updatePostFromTribute(event) {
|
||||
// Trick to let vue-contenteditable know that tribute replaced a mention or hashtag
|
||||
this.$refs.composerInput.oninput(event)
|
||||
console.debug('[Composer] update from tribute', event)
|
||||
this.updateStatusContent()
|
||||
},
|
||||
async createPost(event) {
|
||||
|
||||
const postData = this.getPostData()
|
||||
|
||||
// Trick to validate last mention when the user directly clicks on the "post" button without validating it.
|
||||
const regex = /@([-\w]+)$/
|
||||
const lastMention = postData.get('content').match(regex)
|
||||
if (lastMention) {
|
||||
|
||||
// Ask the server for matching accounts, and wait for the results
|
||||
const result = await this.remoteSearchAccounts(lastMention[1])
|
||||
|
||||
// Validate the last mention only when it matches a single account
|
||||
if (result.data.result.accounts.length === 1) {
|
||||
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.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
|
||||
// }
|
||||
|
||||
// Post message
|
||||
this.loading = true
|
||||
this.$store.dispatch('post', postData).then((response) => {
|
||||
this.loading = false
|
||||
this.replyTo = null
|
||||
this.post = ''
|
||||
this.$refs.composerInput.innerText = this.post
|
||||
this.previewUrls = []
|
||||
this.$store.dispatch('refreshTimeline')
|
||||
// Replace emoji <img> tag with actual emojis.
|
||||
// They will be replaced again with twemoji during rendering
|
||||
const element = this.$refs.composerInput.cloneNode(true)
|
||||
Array.from(element.getElementsByClassName('emoji')).forEach((emoji) => {
|
||||
const em = document.createTextNode(emoji.getAttribute('alt'))
|
||||
emoji.replaceWith(em)
|
||||
})
|
||||
|
||||
let status = element.innerHTML.replace(/<(?!\/div)[^>]+>/gi, '').replace(/<\/div>/gi, '\n').trim()
|
||||
status = he.decode(status)
|
||||
|
||||
const statusData = {
|
||||
content_type: '',
|
||||
media_ids: Object.values(this.attachments).map(preview => preview.data.id),
|
||||
sensitive: false,
|
||||
spoiler_text: '',
|
||||
status,
|
||||
in_reply_to_id: this.replyTo?.id,
|
||||
visibility: this.type,
|
||||
}
|
||||
|
||||
console.debug('[Composer] Posting status', statusData)
|
||||
|
||||
// Post message
|
||||
try {
|
||||
this.loading = true
|
||||
await this.$store.dispatch('post', statusData)
|
||||
} finally {
|
||||
this.loading = false
|
||||
this.replyTo = null
|
||||
this.$refs.composerInput.innerText = ''
|
||||
this.attachments = {}
|
||||
this.$store.dispatch('refreshTimeline')
|
||||
}
|
||||
},
|
||||
closeReply() {
|
||||
this.replyTo = null
|
||||
|
@ -540,13 +387,13 @@ export default {
|
|||
this.$store.commit('setComposerDisplayStatus', false)
|
||||
},
|
||||
remoteSearchAccounts(text) {
|
||||
return axios.get(generateUrl('apps/social/api/v1/global/accounts/search?search=' + text))
|
||||
return axios.get(generateUrl('apps/social/api/v1/global/accounts/search'), { params: { search: text } })
|
||||
},
|
||||
remoteSearchHashtags(text) {
|
||||
return axios.get(generateUrl('apps/social/api/v1/global/tags/search?search=' + text))
|
||||
return axios.get(generateUrl('apps/social/api/v1/global/tags/search'), { params: { search: text } })
|
||||
},
|
||||
deletePreview(index) {
|
||||
this.previewUrls.splice(index, 1)
|
||||
deletePreview(key) {
|
||||
this.$delete(this.attachments, key)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
@ -19,10 +19,10 @@ SPDX-License-Identifier: AGPL-3.0-or-later
|
|||
</div>
|
||||
</div>
|
||||
<div class="preview-grid">
|
||||
<PreviewGridItem v-for="(item, index) in miniatures"
|
||||
:key="index"
|
||||
<PreviewGridItem v-for="(item, randomKey) in miniatures"
|
||||
:key="randomKey"
|
||||
:preview="item"
|
||||
:index="index"
|
||||
:random-key="randomKey"
|
||||
@delete="deletePreview" />
|
||||
</div>
|
||||
</div>
|
||||
|
@ -31,6 +31,7 @@ SPDX-License-Identifier: AGPL-3.0-or-later
|
|||
<script>
|
||||
import PreviewGridItem from './PreviewGridItem.vue'
|
||||
import FileUpload from 'vue-material-design-icons/FileUpload.vue'
|
||||
import { translate } from '@nextcloud/l10n'
|
||||
|
||||
export default {
|
||||
name: 'PreviewGrid',
|
||||
|
@ -47,15 +48,18 @@ export default {
|
|||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
/** @type {import('vue').PropType<Object<string, import('./Composer.vue').LocalAttachment>>} */
|
||||
miniatures: {
|
||||
type: Array,
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
deletePreview(index) {
|
||||
this.$emit('deleted', index)
|
||||
deletePreview(randomKey) {
|
||||
this.$emit('deleted', randomKey)
|
||||
},
|
||||
|
||||
t: translate,
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -1,87 +1,46 @@
|
|||
<template>
|
||||
<div class="preview-item-wrapper">
|
||||
<div class="preview-item" :style="backgroundStyle">
|
||||
<div class="preview-item">
|
||||
<MediaAttachment :attachment="preview.data" />
|
||||
|
||||
<div class="preview-item__actions">
|
||||
<NcButton type="tertiary-no-background" @click="$emit('delete', index)">
|
||||
<NcButton type="tertiary-no-background" @click="$emit('delete', randomKey)">
|
||||
<template #icon>
|
||||
<Close :size="16" fill-color="white" />
|
||||
</template>
|
||||
<span>{{ t('social', 'Delete') }}</span>
|
||||
</NcButton>
|
||||
<!--
|
||||
<NcButton type="tertiary-no-background" @click="showModal">
|
||||
<template #icon>
|
||||
<Edit :size="16" fill-color="white" />
|
||||
</template>
|
||||
<span>{{ t('social', 'Edit') }}</span>
|
||||
</NcButton>
|
||||
-->
|
||||
</div>
|
||||
|
||||
<!--
|
||||
<div v-if="preview.description.length === 0" class="description-warning">
|
||||
{{ t('social', 'No description added') }}
|
||||
</div>
|
||||
|
||||
<NcModal v-if="modal" size="small" @close="closeModal">
|
||||
<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" />
|
||||
<NcButton type="primary" @click="closeModal">
|
||||
{{ t('social', 'Close') }}
|
||||
</NcButton>
|
||||
</div>
|
||||
</NcModal>
|
||||
-->
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Close from 'vue-material-design-icons/Close.vue'
|
||||
// import Edit from 'vue-material-design-icons/Pencil.vue'
|
||||
// import NcModal from '@nextcloud/vue/dist/Components/NcModal.js'
|
||||
import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
|
||||
import { translate } from '@nextcloud/l10n'
|
||||
import MediaAttachment from '../MediaAttachment.vue'
|
||||
|
||||
export default {
|
||||
name: 'PreviewGridItem',
|
||||
components: {
|
||||
Close,
|
||||
// Edit,
|
||||
// NcModal,
|
||||
NcButton,
|
||||
MediaAttachment,
|
||||
},
|
||||
props: {
|
||||
/** @type {import('vue').PropType<import('./Composer.vue').LocalAttachment>} */
|
||||
preview: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
index: {
|
||||
type: Number,
|
||||
randomKey: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
modal: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
backgroundStyle() {
|
||||
return {
|
||||
backgroundImage: `url("${this.preview.url}")`,
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
showModal() {
|
||||
this.modal = true
|
||||
},
|
||||
closeModal() {
|
||||
this.modal = false
|
||||
},
|
||||
t: translate,
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
@ -95,7 +54,7 @@ export default {
|
|||
|
||||
.preview-item {
|
||||
border-radius: var(--border-radius-large);
|
||||
background-color: #000;
|
||||
background: var(--color-background-darker);
|
||||
background-position: 50%;
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
|
@ -109,6 +68,9 @@ export default {
|
|||
}
|
||||
|
||||
&__actions {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
background: linear-gradient(180deg,rgba(0,0,0,.8),rgba(0,0,0,.35) 80%,transparent);
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
|
|
|
@ -0,0 +1,137 @@
|
|||
<!--
|
||||
- @copyright Copyright (c) 2018 Julius Härtl <jus@bitgrid.net>
|
||||
- @copyright Copyright (c) 2022 Carl Schwan <carl@carlschwan.eu>
|
||||
-
|
||||
- @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>
|
||||
<NcButton :value="currentVisibilityPostLabel"
|
||||
:disabled="!disabled"
|
||||
native-type="submit"
|
||||
type="primary"
|
||||
@click.prevent="handleClick">
|
||||
<template #icon>
|
||||
<Send title="" :size="22" decorative />
|
||||
</template>
|
||||
{{ postTo }}
|
||||
</NcButton>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import Send from 'vue-material-design-icons/Send.vue'
|
||||
import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
|
||||
|
||||
export default {
|
||||
name: 'SubmitStatusButton',
|
||||
components: {
|
||||
NcButton,
|
||||
Send,
|
||||
},
|
||||
props: {
|
||||
type: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
/** @return {string} */
|
||||
postTo() {
|
||||
switch (this.type) {
|
||||
case 'public':
|
||||
case 'unlisted':
|
||||
return t('social', 'Post')
|
||||
case 'followers':
|
||||
return t('social', 'Post to followers')
|
||||
case 'direct':
|
||||
return t('social', 'Post to mentioned users')
|
||||
}
|
||||
return ''
|
||||
},
|
||||
/** @return {string} */
|
||||
currentVisibilityPostLabel() {
|
||||
return this.visibilityPostLabel(this.type)
|
||||
},
|
||||
/** @return {Function} */
|
||||
visibilityPostLabel() {
|
||||
return (type) => {
|
||||
if (typeof type === 'undefined') {
|
||||
type = this.type
|
||||
}
|
||||
switch (type) {
|
||||
case 'public':
|
||||
return t('social', 'Post publicly')
|
||||
case 'followers':
|
||||
return t('social', 'Post to followers')
|
||||
case 'direct':
|
||||
return t('social', 'Post to recipients')
|
||||
case 'unlisted':
|
||||
return t('social', 'Post unlisted')
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
handleClick() {
|
||||
this.$emit('click')
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.new-post {
|
||||
padding: 10px;
|
||||
background-color: var(--color-main-background);
|
||||
position: sticky;
|
||||
z-index: 100;
|
||||
margin-bottom: 10px;
|
||||
top: 0;
|
||||
|
||||
&-form {
|
||||
flex-grow: 1;
|
||||
position: relative;
|
||||
top: -10px;
|
||||
margin-left: 39px;
|
||||
&__emoji-picker {
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
input[type=submit].inline {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
margin: 0;
|
||||
padding: 13px;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
opacity: 0.3;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
</style>
|
|
@ -0,0 +1,121 @@
|
|||
<!--
|
||||
- @copyright Copyright (c) 2018 Julius Härtl <jus@bitgrid.net>
|
||||
- @copyright Copyright (c) 2022 Carl Schwan <carl@carlschwan.eu>
|
||||
-
|
||||
- @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-click-outside="hidePopoverMenu" class="popovermenu-parent">
|
||||
<NcButton v-tooltip="t('social', 'Visibility')"
|
||||
type="tertiary"
|
||||
:class="currentVisibilityIconClass"
|
||||
@click.prevent="togglePopoverMenu" />
|
||||
<div :class="{open: menuOpened}" class="popovermenu">
|
||||
<NcPopoverMenu :menu="visibilityPopover" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
|
||||
import NcPopoverMenu from '@nextcloud/vue/dist/Components/NcPopoverMenu.js'
|
||||
import { translate } from '@nextcloud/l10n'
|
||||
|
||||
export default {
|
||||
name: 'VisibilitySelect',
|
||||
components: {
|
||||
NcPopoverMenu,
|
||||
NcButton,
|
||||
},
|
||||
props: {
|
||||
type: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
menuOpened: false,
|
||||
test: false,
|
||||
typeToClass: {
|
||||
public: 'icon-link',
|
||||
followers: 'icon-contacts-dark',
|
||||
direct: 'icon-external',
|
||||
unlisted: 'icon-password',
|
||||
},
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
/** @return {string} */
|
||||
currentVisibilityIconClass() {
|
||||
return this.typeToClass[this.type]
|
||||
},
|
||||
/** @return {Array} */
|
||||
visibilityPopover() {
|
||||
return [
|
||||
{
|
||||
action: () => this.switchType('public'),
|
||||
icon: this.typeToClass.public,
|
||||
active: this.type === 'public',
|
||||
text: t('social', 'Public'),
|
||||
longtext: t('social', 'Post to public timelines'),
|
||||
},
|
||||
{
|
||||
action: () => this.switchType('unlisted'),
|
||||
icon: this.typeToClass.unlisted,
|
||||
active: this.type === 'unlisted',
|
||||
text: t('social', 'Unlisted'),
|
||||
longtext: t('social', 'Do not post to public timelines'),
|
||||
},
|
||||
{
|
||||
action: () => this.switchType('followers'),
|
||||
icon: this.typeToClass.followers,
|
||||
active: this.type === 'followers',
|
||||
text: t('social', 'Followers'),
|
||||
longtext: t('social', 'Post to followers only'),
|
||||
},
|
||||
{
|
||||
action: () => this.switchType('direct'),
|
||||
icon: this.typeToClass.direct,
|
||||
active: this.type === 'direct',
|
||||
text: t('social', 'Direct'),
|
||||
longtext: t('social', 'Post to mentioned users only'),
|
||||
},
|
||||
]
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
togglePopoverMenu() {
|
||||
this.menuOpened = !this.menuOpened
|
||||
},
|
||||
|
||||
hidePopoverMenu() {
|
||||
this.menuOpened = false
|
||||
},
|
||||
|
||||
switchType(type) {
|
||||
this.$emit('update:type', type)
|
||||
this.menuOpened = false
|
||||
localStorage.setItem('social.lastPostType', type)
|
||||
},
|
||||
|
||||
t: translate,
|
||||
},
|
||||
}
|
||||
</script>
|
|
@ -16,10 +16,10 @@ const UFE0Fg = /\uFE0F/g
|
|||
export default {
|
||||
name: 'Emoji',
|
||||
props: {
|
||||
emoji: { type: String, default: '' },
|
||||
},
|
||||
data() {
|
||||
return {}
|
||||
emoji: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
/**
|
||||
|
|
|
@ -21,39 +21,47 @@
|
|||
-->
|
||||
|
||||
<template>
|
||||
<div class="emptycontent">
|
||||
<img v-if="item.image"
|
||||
:src="imageUrl"
|
||||
class="icon-illustration"
|
||||
alt="">
|
||||
<h2>{{ item.title }}</h2>
|
||||
<p>{{ item.description }}</p>
|
||||
</div>
|
||||
<NcEmptyContent :title="item.title" :description="item.description">
|
||||
<template v-if="item.image" #icon>
|
||||
<img class="empty-content__image"
|
||||
:src="imageUrl"
|
||||
alt="">
|
||||
</template>
|
||||
</NcEmptyContent>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import { linkTo } from '@nextcloud/router'
|
||||
import NcEmptyContent from '@nextcloud/vue/dist/Components/NcEmptyContent.js'
|
||||
|
||||
export default {
|
||||
name: 'EmptyContent',
|
||||
components: {
|
||||
NcEmptyContent,
|
||||
},
|
||||
props: {
|
||||
item: { type: Object, default: () => {} },
|
||||
item: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
/** @return {string} */
|
||||
imageUrl() {
|
||||
return OC.linkTo('social', this.item.image)
|
||||
return linkTo('social', this.item.image)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style scoped>
|
||||
.emptycontent {
|
||||
margin-top: 5vh;
|
||||
}
|
||||
.empty-content__image {
|
||||
height: 256px;
|
||||
width: 256px;
|
||||
}
|
||||
|
||||
.emptycontent .icon-illustration {
|
||||
height: 256px;
|
||||
width: 256px;
|
||||
margin: 0;
|
||||
opacity: 1;
|
||||
}
|
||||
:deep(.empty-content__icon) {
|
||||
opacity: 1;
|
||||
margin-bottom: 90px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -22,8 +22,8 @@
|
|||
|
||||
<template>
|
||||
<!-- Show button only if user is authenticated and she is not the same as the account viewed -->
|
||||
<div v-if="!serverData.public && accountInfo && accountInfo.viewerLink!='viewer'">
|
||||
<div v-if="isCurrentUserFollowing"
|
||||
<div v-if="!serverData.public && relationship !== undefined">
|
||||
<div v-if="relationship.following"
|
||||
class="follow-button-container">
|
||||
<NcButton :disabled="loading"
|
||||
class="follow-button follow-button--following"
|
||||
|
@ -87,16 +87,20 @@ export default {
|
|||
}
|
||||
},
|
||||
computed: {
|
||||
/** @return {boolean} */
|
||||
isCurrentUserFollowing() {
|
||||
return this.$store.getters.isFollowingUser(this.account)
|
||||
return this.$store.getters.isFollowingUser(this.profileAccount)
|
||||
},
|
||||
/** @return {import('../types/Mastodon.js').Account} */
|
||||
currentAccount() {
|
||||
return this.$store.getters.currentAccount
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async follow() {
|
||||
try {
|
||||
this.loading = true
|
||||
await this.$store.dispatch('followAccount', { currentAccount: this.cloudId, accountToFollow: this.account })
|
||||
} catch {
|
||||
await this.$store.dispatch('followAccount', { currentAccount: this.cloudId, accountToFollow: this.profileAccount })
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
|
@ -104,8 +108,7 @@ export default {
|
|||
async unfollow() {
|
||||
try {
|
||||
this.loading = true
|
||||
await this.$store.dispatch('unfollowAccount', { currentAccount: this.cloudId, accountToUnfollow: this.account })
|
||||
} catch {
|
||||
await this.$store.dispatch('unfollowAccount', { currentAccount: this.cloudId, accountToUnfollow: this.profileAccount })
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
|
|
|
@ -0,0 +1,77 @@
|
|||
<template>
|
||||
<div class="attachment" @click="$emit('click')">
|
||||
<canvas v-if="!previewLoaded" ref="canvas" class="attachment__blurhash" />
|
||||
<img v-if="attachment !== null"
|
||||
class="attachment__preview"
|
||||
:src="attachment.preview_url"
|
||||
@load="previewLoaded = true">
|
||||
<NcLoadingIcon v-if="attachment === null || !previewLoaded" :size="40" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { decode } from 'blurhash'
|
||||
import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon.js'
|
||||
|
||||
export default {
|
||||
name: 'MediaAttachment',
|
||||
components: {
|
||||
NcLoadingIcon,
|
||||
},
|
||||
props: {
|
||||
/** @type {import('vue').PropType<import('../types/Mastodon').MediaAttachment>} */
|
||||
attachment: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
previewLoaded: false,
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
attachment() {
|
||||
this.drawBlurhash()
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.drawBlurhash()
|
||||
},
|
||||
methods: {
|
||||
drawBlurhash() {
|
||||
if (this.attachment?.meta.small.width === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
const ctx = this.$refs.canvas.getContext('2d')
|
||||
const imageData = ctx.createImageData(this.attachment.meta.small.width, this.attachment.meta.small.height)
|
||||
const pixels = decode(this.attachment.blurhash, this.attachment.meta.small.width, this.attachment.meta.small.height)
|
||||
imageData.data.set(pixels)
|
||||
ctx.putImageData(imageData, 0, 0)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.attachment {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
&__blurhash, &__preview {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.loading-icon {
|
||||
position: absolute;
|
||||
top: calc(50% - 20px);
|
||||
left: calc(50% - 20px);
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -1,48 +1,39 @@
|
|||
import Vue from 'vue'
|
||||
import Emoji from './Emoji.vue'
|
||||
|
||||
/**
|
||||
* @typedef {object} MessageSource
|
||||
* @property {Array} tag
|
||||
* @property {string} content
|
||||
*/
|
||||
|
||||
export default Vue.component('MessageContent', {
|
||||
props: {
|
||||
source: {
|
||||
item: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
render(createElement) {
|
||||
return formatMessage(createElement, this.source)
|
||||
return formatMessage(createElement, this.item)
|
||||
},
|
||||
})
|
||||
|
||||
/**
|
||||
* Transform the message source into Vue elements
|
||||
* Transform the Status into Vue elements
|
||||
*
|
||||
* filters out all tags except <br />, <p>, <span> and <a>.
|
||||
*
|
||||
* Links that are hashtags or mentions are rewritten to link to the local profile or hashtag page
|
||||
* All external links have `rel="nofollow noopener noreferrer"` and `target="_blank"` set.
|
||||
*
|
||||
* All attributes other than `href` for links are stripped from the source
|
||||
* All attributes other than `href` for links are stripped from the content
|
||||
*
|
||||
* @param {Function} createElement
|
||||
* @param {MessageSource} source
|
||||
* @param {import('../types/Mastodon').Status} item
|
||||
*/
|
||||
export function formatMessage(createElement, source) {
|
||||
if (!source.tag) {
|
||||
source.tag = []
|
||||
export function formatMessage(createElement, item) {
|
||||
if (!item.tags) {
|
||||
item.tags = []
|
||||
}
|
||||
const mentions = source.tag.filter(tag => tag.type === 'Mention')
|
||||
const hashtags = source.tag.filter(tag => tag.type === 'Hashtag')
|
||||
|
||||
const parser = new DOMParser()
|
||||
const dom = parser.parseFromString(`<div id="rootwrapper">${source.content}</div>`, 'text/html')
|
||||
const dom = parser.parseFromString(`<div id="rootwrapper">${item.content}</div>`, 'text/html')
|
||||
const element = dom.getElementById('rootwrapper')
|
||||
const cleaned = cleanCopy(createElement, element, { mentions, hashtags })
|
||||
const cleaned = cleanCopy(createElement, element, item)
|
||||
return cleaned
|
||||
}
|
||||
|
||||
|
@ -50,7 +41,7 @@ export function formatMessage(createElement, source) {
|
|||
*
|
||||
* @param {Function} createElement
|
||||
* @param {HTMLElement} node
|
||||
* @param {object} context
|
||||
* @param {import('../types/Mastodon').Status} context
|
||||
*/
|
||||
function domToVue(createElement, node, context) {
|
||||
switch (node.tagName) {
|
||||
|
@ -133,7 +124,7 @@ function transformText(createElement, text) {
|
|||
*
|
||||
* @param {Function} createElement
|
||||
* @param {HTMLElement} node
|
||||
* @param {object} context
|
||||
* @param {import('../types/Mastodon').Status} context
|
||||
*/
|
||||
function cleanCopy(createElement, node, context) {
|
||||
const children = Array.from(node.childNodes).map(node => domToVue(createElement, node, context))
|
||||
|
@ -144,8 +135,7 @@ function cleanCopy(createElement, node, context) {
|
|||
*
|
||||
* @param {Function} createElement
|
||||
* @param {HTMLLinkElement} node
|
||||
* @param {object} context
|
||||
* @param {Array} context.mentions
|
||||
* @param {import('../types/Mastodon').Status} context
|
||||
*/
|
||||
function cleanLink(createElement, node, context) {
|
||||
const type = getLinkType(node.className)
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
<template>
|
||||
<div class="post-attachments">
|
||||
<div v-for="(item, index) in attachments"
|
||||
:key="index"
|
||||
class="post-attachment"
|
||||
@click="showModal(index)">
|
||||
<img v-if="item.mimeType.startsWith('image/')" :src="imageUrl(item)">
|
||||
<div v-else>
|
||||
{{ item }}
|
||||
<div class="attachments-container">
|
||||
<div v-for="(item, index) in attachementsSlice"
|
||||
:key="index"
|
||||
class="attachment"
|
||||
@click="showModal(index)">
|
||||
<MediaAttachment :attachment="item" />
|
||||
</div>
|
||||
<div v-if="attachments.length > 4" class="attachment more-attachments">
|
||||
+
|
||||
</div>
|
||||
</div>
|
||||
<NcModal v-if="modal"
|
||||
|
@ -14,10 +16,10 @@
|
|||
:has-next="current < (attachments.length - 1)"
|
||||
size="full"
|
||||
@close="closeModal"
|
||||
@previous="showPrevious"
|
||||
@next="showNext">
|
||||
<div class="modal__content">
|
||||
<canvas ref="modalCanvas" />
|
||||
@previous="current--"
|
||||
@next="current++">
|
||||
<div class="attachment__viewer">
|
||||
<img :src="attachments[current].url" :alt="attachments[current].description">
|
||||
</div>
|
||||
</NcModal>
|
||||
</div>
|
||||
|
@ -26,17 +28,19 @@
|
|||
<script>
|
||||
import serverData from '../mixins/serverData.js'
|
||||
import NcModal from '@nextcloud/vue/dist/Components/NcModal.js'
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
import MediaAttachment from './MediaAttachment.vue'
|
||||
|
||||
export default {
|
||||
name: 'PostAttachment',
|
||||
components: {
|
||||
NcModal,
|
||||
MediaAttachment,
|
||||
},
|
||||
mixins: [
|
||||
serverData,
|
||||
],
|
||||
props: {
|
||||
/** @type {import('vue').PropType<import('../types/Mastodon.js').MediaAttachment[]>} */
|
||||
attachments: {
|
||||
type: Array,
|
||||
default: Array,
|
||||
|
@ -45,90 +49,72 @@ export default {
|
|||
data() {
|
||||
return {
|
||||
modal: false,
|
||||
current: '',
|
||||
current: 0,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* @function imageUrl
|
||||
* @description Returns the URL where to get a resized version of the attachement
|
||||
* @param {object} item - The attachment
|
||||
* @return {string} The URL
|
||||
*/
|
||||
imageUrl(item) {
|
||||
if (this.serverData.public) {
|
||||
return generateUrl('/apps/social/document/public/resized?id=' + item.id)
|
||||
computed: {
|
||||
/** @return {import('../types/Mastodon.js').MediaAttachment[]} */
|
||||
attachementsSlice() {
|
||||
if (this.attachments.length <= 4) {
|
||||
return this.attachments
|
||||
} else {
|
||||
return generateUrl('/apps/social/document/get/resized?id=' + item.id)
|
||||
return this.attachments.slice(0, 3)
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @function displayImage
|
||||
* @description Displays the currently selected attachment's image
|
||||
*/
|
||||
displayImage() {
|
||||
const canvas = this.$refs.modalCanvas
|
||||
const ctx = canvas.getContext('2d')
|
||||
const img = new Image()
|
||||
img.onload = function() {
|
||||
let width = img.width
|
||||
let height = img.height
|
||||
if (width > window.innerWidth) {
|
||||
height = height * (window.innerWidth / width)
|
||||
width = window.innerWidth
|
||||
}
|
||||
if (height > window.innerHeight) {
|
||||
width = width * (window.innerHeight / height)
|
||||
height = window.innerHeight
|
||||
}
|
||||
canvas.width = width
|
||||
canvas.height = height
|
||||
ctx.drawImage(img, 0, 0, width, height)
|
||||
}
|
||||
img.src = generateUrl('/apps/social/document/get?id=' + this.attachments[this.current].id)
|
||||
},
|
||||
showModal(idx) {
|
||||
this.current = idx
|
||||
this.displayImage()
|
||||
},
|
||||
methods: {
|
||||
showModal(index) {
|
||||
this.current = index
|
||||
this.modal = true
|
||||
},
|
||||
closeModal() {
|
||||
this.modal = false
|
||||
},
|
||||
showPrevious() {
|
||||
this.current--
|
||||
this.displayImage()
|
||||
},
|
||||
showNext() {
|
||||
this.current++
|
||||
this.displayImage()
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.post-attachments {
|
||||
margin-top: 12px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
overflow-x: scroll;
|
||||
|
||||
.post-attachment {
|
||||
height: 100px;
|
||||
object-fit: cover;
|
||||
.attachments-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 2px;
|
||||
margin-top: 12px;
|
||||
width: 100%;
|
||||
border-radius: var(--border-radius-large);
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
height: 40vh;
|
||||
|
||||
> * {
|
||||
.attachment {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
flex-basis: calc(50% - 2px);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
.more-attachments {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 42px;
|
||||
line-height: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.attachment__viewer {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
align-content: center;
|
||||
justify-items: center;
|
||||
padding: 10%;
|
||||
box-sizing: border-box;
|
||||
|
||||
img {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
|
||||
<template>
|
||||
<div v-if="profileAccount && accountInfo" class="user-profile">
|
||||
<NcAvatar v-if="accountInfo.local"
|
||||
<NcAvatar v-if="isLocal"
|
||||
:user="localUid"
|
||||
:disable-tooltip="true"
|
||||
:size="128" />
|
||||
|
@ -32,32 +32,32 @@
|
|||
:size="128" />
|
||||
<h2>{{ displayName }}</h2>
|
||||
<!-- TODO: we have no details, timeline and follower list for non-local accounts for now -->
|
||||
<ul v-if="accountInfo.details && accountInfo.local" class="user-profile__info user-profile__sections">
|
||||
<ul v-if="isLocal" class="user-profile__info user-profile__sections">
|
||||
<li>
|
||||
<router-link :to="{ name: 'profile', params: { account: uid } }" class="icon-category-monitoring">
|
||||
{{ getCount('post') }} {{ t('social', 'posts') }}
|
||||
{{ accountInfo.statuses_count }} {{ t('social', 'posts') }}
|
||||
</router-link>
|
||||
</li>
|
||||
<li>
|
||||
<router-link :to="{ name: 'profile.following', params: { account: uid } }" class="icon-category-social">
|
||||
{{ getCount('following') }} {{ t('social', 'following') }}
|
||||
{{ accountInfo.following_count }} {{ t('social', 'following') }}
|
||||
</router-link>
|
||||
</li>
|
||||
<li>
|
||||
<router-link :to="{ name: 'profile.followers', params: { account: uid } }" class="icon-category-social">
|
||||
{{ getCount('followers') }} {{ t('social', 'followers') }}
|
||||
{{ accountInfo.followers_count }} {{ t('social', 'followers') }}
|
||||
</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
<p class="user-profile__info">
|
||||
<a :href="accountInfo.url" target="_blank">@{{ accountInfo.account }}</a>
|
||||
<a :href="accountInfo.url" target="_blank">@{{ accountInfo.acct }}</a>
|
||||
</p>
|
||||
|
||||
<p v-if="accountInfo.website" class="user-profile__info">
|
||||
{{ t('social', 'Website') }}: <a :href="accountInfo.website.value">{{ accountInfo.website.value }}</a>
|
||||
<p v-if="website" class="user-profile__info">
|
||||
{{ t('social', 'Website') }}: <a :href="website.value">{{ website.value }}</a>
|
||||
</p>
|
||||
|
||||
<FollowButton class="user-profile__info" :account="accountInfo.account" :uid="uid" />
|
||||
<FollowButton class="user-profile__info" :account="accountInfo.acct" :uid="uid" />
|
||||
<NcButton v-if="serverData.public"
|
||||
class="user-profile__info primary"
|
||||
@click="followRemote">
|
||||
|
@ -72,9 +72,9 @@ 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'
|
||||
import { translate } from '@nextcloud/l10n'
|
||||
|
||||
export default {
|
||||
name: 'ProfileInfo',
|
||||
|
@ -87,7 +87,6 @@ export default {
|
|||
accountMixins,
|
||||
currentUser,
|
||||
serverData,
|
||||
follow,
|
||||
],
|
||||
props: {
|
||||
uid: {
|
||||
|
@ -101,31 +100,30 @@ export default {
|
|||
}
|
||||
},
|
||||
computed: {
|
||||
/** @return {string} */
|
||||
localUid() {
|
||||
// Returns only the local part of a username
|
||||
return (this.uid.indexOf('@') === -1) ? this.uid : this.uid.slice(0, this.uid.indexOf('@'))
|
||||
},
|
||||
/** @return {string} */
|
||||
displayName() {
|
||||
if (typeof this.accountInfo.name !== 'undefined' && this.accountInfo.name !== '') {
|
||||
return this.accountInfo.name
|
||||
}
|
||||
if (typeof this.accountInfo.preferredUsername !== 'undefined' && this.accountInfo.preferredUsername !== '') {
|
||||
return this.accountInfo.preferredUsername
|
||||
}
|
||||
return this.profileAccount
|
||||
},
|
||||
getCount() {
|
||||
const account = this.accountInfo
|
||||
return (field) => account.details.count ? account.details.count[field] : ''
|
||||
return this.accountInfo.display_name ?? this.accountInfo.username ?? this.profileAccount
|
||||
},
|
||||
/** @return {string} */
|
||||
avatarUrl() {
|
||||
return generateUrl('/apps/social/api/v1/global/actor/avatar?id=' + this.accountInfo.id)
|
||||
},
|
||||
/** @return {import('../types/Mastodon.js').Field} */
|
||||
website() {
|
||||
return this.accountInfo.fields.find(field => field.name === 'Website')
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
followRemote() {
|
||||
window.open(generateUrl('/apps/social/api/v1/ostatus/followRemote/' + encodeURI(this.localUid)), 'followRemote', 'width=433,height=600toolbar=no,menubar=no,scrollbars=yes,resizable=yes')
|
||||
},
|
||||
|
||||
t: translate,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -57,6 +57,7 @@ import UserEntry from './UserEntry.vue'
|
|||
import axios from '@nextcloud/axios'
|
||||
import Trend from 'vuetrend'
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
import { translate } from '@nextcloud/l10n'
|
||||
|
||||
export default {
|
||||
name: 'Search',
|
||||
|
@ -80,6 +81,7 @@ export default {
|
|||
}
|
||||
},
|
||||
computed: {
|
||||
/** @return {import('../types/Mastodon.js').Account[]} */
|
||||
allResults() {
|
||||
if (this.results.accounts) {
|
||||
if (this.results.accounts.exact) {
|
||||
|
@ -138,6 +140,8 @@ export default {
|
|||
remoteSearch(term) {
|
||||
return axios.get(generateUrl('apps/social/api/v1/global/account/info?account=' + term))
|
||||
},
|
||||
|
||||
t: translate,
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -1,14 +1,15 @@
|
|||
<template>
|
||||
<div v-if="item.actor_info" class="post-avatar">
|
||||
<NcAvatar v-if="item.local"
|
||||
<div v-if="item.account" class="post-avatar">
|
||||
<NcAvatar v-if="isLocal"
|
||||
class="messages__avatar__icon"
|
||||
:show-user-status="false"
|
||||
menu-position="left"
|
||||
:user="userTest"
|
||||
:display-name="item.actor_info.account"
|
||||
:display-name="item.account.acct"
|
||||
:url="item.account.avatar"
|
||||
:disable-tooltip="true" />
|
||||
<NcAvatar v-else
|
||||
:url="avatarUrl"
|
||||
:url="item.account.avatar"
|
||||
:disable-tooltip="true" />
|
||||
</div>
|
||||
</template>
|
||||
|
@ -22,17 +23,22 @@ export default {
|
|||
NcAvatar,
|
||||
},
|
||||
props: {
|
||||
/** @type {import('vue').PropType<import('../types/Mastodon.js').Status>} */
|
||||
item: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
/**
|
||||
* @return {string}
|
||||
*/
|
||||
userTest() {
|
||||
return this.item.actor_info.preferredUsername
|
||||
return this.item.account.display_name
|
||||
},
|
||||
avatarUrl() {
|
||||
return OC.generateUrl('/apps/social/api/v1/global/actor/avatar?id=' + this.item.attributedTo)
|
||||
/** @return {boolean} */
|
||||
isLocal() {
|
||||
return !this.item.account.acct.includes('@')
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
@ -1,36 +1,39 @@
|
|||
<template>
|
||||
<div :class="['timeline-entry', hasHeader ? 'with-header' : '']">
|
||||
<div v-if="item.type === 'SocialAppNotification'" class="notification">
|
||||
<div v-if="isNotification" class="notification">
|
||||
<Bell :size="22" />
|
||||
<span class="notification-action">
|
||||
{{ actionSummary }}
|
||||
</span>
|
||||
</div>
|
||||
<template v-else-if="item.type === 'Announce'">
|
||||
<template v-else-if="isBoost">
|
||||
<div class="container-icon-boost boost">
|
||||
<span class="icon-boost" />
|
||||
</div>
|
||||
<div class="boost">
|
||||
<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) }}
|
||||
<router-link v-if="!isProfilePage && item.account"
|
||||
:to="{ name: 'profile', params: { account: item.account.username } }">
|
||||
<span v-tooltip.bottom="item.account.acct" class="post-author">
|
||||
{{ item.account.display_name }}
|
||||
</span>
|
||||
</router-link>
|
||||
<a v-else :href="item.attributedTo">
|
||||
<a v-else :href="item.account.id">
|
||||
<span class="post-author-id">
|
||||
{{ item.attributedTo }}
|
||||
{{ item.account.id }}
|
||||
</span>
|
||||
</a>
|
||||
{{ boosted }}
|
||||
{{ t('social', 'boosted') }}
|
||||
</div>
|
||||
</template>
|
||||
<UserEntry v-if="item.type === 'SocialAppNotification' && item.details.actor" :key="item.details.actor.id" :item="item.details.actor" />
|
||||
<UserEntry v-if="isNotification && notificationIsAboutAnAccount"
|
||||
:key="item.account.id"
|
||||
:item="item.account" />
|
||||
<template v-else>
|
||||
<div class="wrapper">
|
||||
<TimelineAvatar class="entry__avatar" :item="entryContent" />
|
||||
<TimelinePost class="entry__content"
|
||||
:item="entryContent"
|
||||
:parent-announce="isBoost" />
|
||||
:type="type" />
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
@ -41,6 +44,7 @@ import TimelinePost from './TimelinePost.vue'
|
|||
import TimelineAvatar from './TimelineAvatar.vue'
|
||||
import UserEntry from './UserEntry.vue'
|
||||
import Bell from 'vue-material-design-icons/Bell.vue'
|
||||
import { translate } from '@nextcloud/l10n'
|
||||
|
||||
export default {
|
||||
name: 'TimelineEntry',
|
||||
|
@ -51,77 +55,91 @@ export default {
|
|||
Bell,
|
||||
},
|
||||
props: {
|
||||
/** @type {import('vue').PropType<import('../types/Mastodon.js').Status|import('../types/Mastodon.js').Notification>} */
|
||||
item: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
isProfilePage: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
/**
|
||||
* @return {import('../types/Mastodon.js').Status}
|
||||
*/
|
||||
entryContent() {
|
||||
if (this.item.type === 'Announce') {
|
||||
return this.item.cache[this.item.object].object
|
||||
} else if (this.item.type === 'SocialAppNotification') {
|
||||
return this.item.details.post
|
||||
if (this.isNotification) {
|
||||
return this.notification.status
|
||||
} else {
|
||||
return this.item
|
||||
}
|
||||
},
|
||||
/** @return {boolean} */
|
||||
isNotification() {
|
||||
return this.item.type !== undefined
|
||||
},
|
||||
/**
|
||||
* @return {boolean}
|
||||
*/
|
||||
isBoost() {
|
||||
if (this.item.type === 'Announce') {
|
||||
return this.item
|
||||
}
|
||||
return {}
|
||||
return this.reblog !== null
|
||||
},
|
||||
/** @return {import('../types/Mastodon.js').Notification} */
|
||||
notification() {
|
||||
return this.item
|
||||
},
|
||||
/** @return {import('../types/Mastodon.js').Status} */
|
||||
status() {
|
||||
return this.item
|
||||
},
|
||||
/** @return {boolean} */
|
||||
notificationIsAboutAnAccount() {
|
||||
return this.notification.type in ['follow', 'follow_request', 'admin.sign_up', 'admin.report']
|
||||
},
|
||||
/**
|
||||
* @return {boolean}
|
||||
*/
|
||||
hasHeader() {
|
||||
return this.item.type === 'Announce' || this.item.type === 'SocialAppNotification'
|
||||
},
|
||||
boosted() {
|
||||
return t('social', 'boosted')
|
||||
return this.isBoost || this.isNotification
|
||||
},
|
||||
/**
|
||||
* @return {string}
|
||||
*/
|
||||
actionSummary() {
|
||||
|
||||
let summary = this.item.summary
|
||||
for (const key in this.item.details) {
|
||||
|
||||
const keyword = '{' + key + '}'
|
||||
if (typeof this.item.details[key] !== 'string' && this.item.details[key].length > 1) {
|
||||
|
||||
let concatination = ''
|
||||
for (const stringKey in this.item.details[key]) {
|
||||
|
||||
if (this.item.details[key].length > 3 && stringKey === '3') {
|
||||
// ellipses the actors' list to 3 actors when it's big
|
||||
concatination = concatination.substring(0, concatination.length - 2)
|
||||
concatination += ' and ' + (this.item.details[key].length - 3).toString() + ' other(s), '
|
||||
break
|
||||
} else {
|
||||
concatination += this.item.details[key][stringKey] + ', '
|
||||
}
|
||||
}
|
||||
|
||||
concatination = concatination.substring(0, concatination.length - 2)
|
||||
summary = summary.replace(keyword, concatination)
|
||||
|
||||
} else {
|
||||
summary = summary.replace(keyword, this.item.details[key])
|
||||
}
|
||||
switch (this.notification.type) {
|
||||
case 'mention':
|
||||
return t('social', '{account} mentioned you', { account: this.notification.account.acct })
|
||||
case 'status':
|
||||
return t('social', '{account} has posted a status', { account: this.notification.account.acct })
|
||||
case 'reblog':
|
||||
return t('social', '{account} boosted your post', { account: this.notification.account.acct })
|
||||
case 'follow':
|
||||
return t('social', '{account} started following you', { account: this.notification.account.acct })
|
||||
case 'follow_request':
|
||||
return t('social', '{account} requested to follow you', { account: this.notification.account.acct })
|
||||
case 'favourite':
|
||||
return t('social', '{account} like you post', { account: this.notification.account.acct })
|
||||
case 'poll':
|
||||
return t('social', '{account} as ended the poll', { account: this.notification.account.acct })
|
||||
case 'update':
|
||||
return t('social', '{account} edit a status', { account: this.notification.account.acct })
|
||||
case 'admin.sign_up':
|
||||
return t('social', '{account} signed up', { account: this.notification.account.acct })
|
||||
case 'admin.report':
|
||||
return t('social', '{account} filled a report', { account: this.notification.account.acct })
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
|
||||
return summary
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
userDisplayName(actorInfo) {
|
||||
return actorInfo.name !== '' ? actorInfo.name : actorInfo.preferredUsername
|
||||
},
|
||||
t: translate,
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -23,7 +23,10 @@
|
|||
<template>
|
||||
<div class="social__timeline">
|
||||
<transition-group name="list" tag="div">
|
||||
<TimelineEntry v-for="entry in timeline" :key="entry.id" :item="entry" />
|
||||
<TimelineEntry v-for="entry in timeline"
|
||||
:key="entry.id"
|
||||
:item="entry"
|
||||
:type="type" />
|
||||
</transition-group>
|
||||
<InfiniteLoading ref="infiniteLoading" @infinite="infiniteHandler">
|
||||
<div slot="spinner">
|
||||
|
@ -41,10 +44,13 @@
|
|||
|
||||
<script>
|
||||
import InfiniteLoading from 'vue-infinite-loading'
|
||||
|
||||
import { showError } from '@nextcloud/dialogs'
|
||||
|
||||
import TimelineEntry from './TimelineEntry.vue'
|
||||
import CurrentUserMixin from './../mixins/currentUserMixin.js'
|
||||
import EmptyContent from './EmptyContent.vue'
|
||||
import Logger from '../logger.js'
|
||||
import logger from '../services/logger.js'
|
||||
|
||||
export default {
|
||||
name: 'TimelineList',
|
||||
|
@ -55,7 +61,10 @@ export default {
|
|||
},
|
||||
mixins: [CurrentUserMixin],
|
||||
props: {
|
||||
type: { type: String, default: () => 'home' },
|
||||
type: {
|
||||
type: String,
|
||||
default: () => 'home',
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
@ -87,7 +96,7 @@ export default {
|
|||
title: t('social', 'No global posts found'),
|
||||
description: t('social', 'Posts from federated instances will show up here'),
|
||||
},
|
||||
liked: {
|
||||
favourites: {
|
||||
image: 'img/undraw/likes.svg',
|
||||
title: t('social', 'No liked posts found'),
|
||||
},
|
||||
|
@ -121,9 +130,13 @@ export default {
|
|||
}
|
||||
|
||||
// Fallback
|
||||
Logger.log('Did not find any empty content for this route', { routeType: this.$route.params.type, routeName: this.$route.name })
|
||||
logger.log('Did not find any empty content for this route', { routeType: this.$route.params.type, routeName: this.$route.name })
|
||||
return this.emptyContent.default
|
||||
},
|
||||
|
||||
/**
|
||||
* @return {import('../store/timeline.js').APObject[]}
|
||||
*/
|
||||
timeline() {
|
||||
return this.$store.getters.getTimeline
|
||||
},
|
||||
|
@ -132,22 +145,19 @@ export default {
|
|||
|
||||
},
|
||||
methods: {
|
||||
infiniteHandler($state) {
|
||||
this.$store.dispatch('fetchTimeline', {
|
||||
account: this.currentUser.uid,
|
||||
}).then((response) => {
|
||||
if (response.status === -1) {
|
||||
OC.Notification.showTemporary('Failed to load more timeline entries')
|
||||
console.error('Failed to load more timeline entries', response)
|
||||
$state.complete()
|
||||
return
|
||||
}
|
||||
response.result.length > 0 ? $state.loaded() : $state.complete()
|
||||
}).catch((error) => {
|
||||
OC.Notification.showTemporary('Failed to load more timeline entries')
|
||||
console.error('Failed to load more timeline entries', error)
|
||||
async infiniteHandler($state) {
|
||||
try {
|
||||
const response = await this.$store.dispatch('fetchTimeline', {
|
||||
account: this.currentUser.uid,
|
||||
max_id: this.timeline.length > 0 ? Number.parseInt(this.timeline[this.timeline.length - 1].id) : undefined,
|
||||
})
|
||||
|
||||
response.length > 0 ? $state.loaded() : $state.complete()
|
||||
} catch (error) {
|
||||
showError('Failed to load more timeline entries')
|
||||
logger.error('Failed to load more timeline entries', { error })
|
||||
$state.complete()
|
||||
})
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
@ -2,35 +2,40 @@
|
|||
<div class="post-content">
|
||||
<div class="post-header">
|
||||
<div class="post-author-wrapper">
|
||||
<router-link v-if="item.actor_info"
|
||||
<!-- TODO -->
|
||||
<router-link v-if="item.account"
|
||||
:to="{ name: 'profile',
|
||||
params: { account: (item.local && item.type!=='SocialAppNotification') ? item.actor_info.preferredUsername : item.actor_info.account }
|
||||
params: { account: (isLocal && !isNotification) ? item.account.display_name : item.account.username }
|
||||
}">
|
||||
<span class="post-author">
|
||||
{{ userDisplayName(item.actor_info) }}
|
||||
{{ item.account.display_name }}
|
||||
</span>
|
||||
<span class="post-author-id">
|
||||
@{{ item.actor_info.account }}
|
||||
@{{ item.account.username }}
|
||||
</span>
|
||||
</router-link>
|
||||
<a v-else :href="item.attributedTo">
|
||||
<a v-else :href="item.account.id">
|
||||
<span class="post-author-id">
|
||||
{{ item.attributedTo }}
|
||||
{{ item.account.id }}
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
<a :data-timestamp="timestamp" class="post-timestamp live-relative-timestamp" @click="getSinglePostTimeline">
|
||||
<a :data-timestamp="timestamp"
|
||||
class="post-timestamp live-relative-timestamp"
|
||||
:title="formattedDate"
|
||||
@click="getSinglePostTimeline">
|
||||
{{ relativeTimestamp }}
|
||||
</a>
|
||||
</div>
|
||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||
<div v-if="item.content" class="post-message">
|
||||
<MessageContent :source="source" />
|
||||
<MessageContent :item="item" />
|
||||
</div>
|
||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||
<div v-else class="post-message" v-html="item.actor_info.summary" />
|
||||
<div v-else class="post-message" v-html="item.account.note" />
|
||||
<div v-if="hasAttachments" class="post-attachments">
|
||||
<PostAttachment :attachments="item.attachment" />
|
||||
<!-- TODO: clean media_attachments -->
|
||||
<PostAttachment :attachments="item.media_attachments || []" />
|
||||
</div>
|
||||
<div v-if="$route && $route.params.type !== 'notifications' && !serverData.public" class="post-actions">
|
||||
<NcButton v-tooltip="t('social', 'Reply')"
|
||||
|
@ -64,7 +69,7 @@
|
|||
</template>
|
||||
</NcButton>
|
||||
<NcActions>
|
||||
<NcActionButton v-if="item.actor_info.account === cloudId"
|
||||
<NcActionButton v-if="item.account !== undefined && item.account.acct === currentAccount.acct"
|
||||
icon="icon-delete"
|
||||
@click="remove()">
|
||||
{{ t('social', 'Delete') }}
|
||||
|
@ -90,7 +95,6 @@ import Heart from 'vue-material-design-icons/Heart.vue'
|
|||
import HeartOutline from 'vue-material-design-icons/HeartOutline.vue'
|
||||
import logger from '../services/logger.js'
|
||||
import moment from '@nextcloud/moment'
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
import MessageContent from './MessageContent.js'
|
||||
|
||||
export default {
|
||||
|
@ -108,47 +112,75 @@ export default {
|
|||
},
|
||||
mixins: [currentUser],
|
||||
props: {
|
||||
item: { type: Object, default: () => {} },
|
||||
parentAnnounce: { type: Object, default: () => {} },
|
||||
/** @type {import('vue').PropType<import('../types/Mastodon.js').Status>} */
|
||||
item: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
/**
|
||||
* @return {string}
|
||||
*/
|
||||
relativeTimestamp() {
|
||||
return moment(this.item.published).fromNow()
|
||||
return moment(this.item.created_at).fromNow()
|
||||
},
|
||||
/**
|
||||
* @return {string}
|
||||
*/
|
||||
formattedDate() {
|
||||
return moment(this.item.created_at).format('LLL')
|
||||
},
|
||||
/**
|
||||
* @return {number}
|
||||
*/
|
||||
timestamp() {
|
||||
return Date.parse(this.item.published)
|
||||
},
|
||||
source() {
|
||||
if (!this.item.source && this.item.content) {
|
||||
// local posts don't have a source json
|
||||
return {
|
||||
content: this.item.content,
|
||||
tag: [],
|
||||
}
|
||||
}
|
||||
return JSON.parse(this.item.source)
|
||||
},
|
||||
avatarUrl() {
|
||||
return generateUrl('/apps/social/api/v1/global/actor/avatar?id=' + this.item.attributedTo)
|
||||
return Date.parse(this.item.created_at)
|
||||
},
|
||||
/**
|
||||
* @return {boolean}
|
||||
*/
|
||||
hasAttachments() {
|
||||
return (typeof this.item.attachment !== 'undefined')
|
||||
// TODO: clean media_attachments
|
||||
return (this.item.media_attachments || []).length > 0
|
||||
},
|
||||
/**
|
||||
* @return {boolean}
|
||||
*/
|
||||
isBoosted() {
|
||||
if (typeof this.item.action === 'undefined') {
|
||||
return false
|
||||
}
|
||||
return !!this.item.action.values.boosted
|
||||
return this.item.reblogged === true
|
||||
},
|
||||
/**
|
||||
* @return {boolean}
|
||||
*/
|
||||
|
||||
isLiked() {
|
||||
if (typeof this.item.action === 'undefined') {
|
||||
return false
|
||||
}
|
||||
return !!this.item.action.values.liked
|
||||
return this.item.favourited === true
|
||||
},
|
||||
/**
|
||||
* @return {object}
|
||||
*/
|
||||
richParameters() {
|
||||
return {}
|
||||
},
|
||||
/**
|
||||
* @return {boolean}
|
||||
*/
|
||||
isLocal() {
|
||||
return !this.item.account.acct.includes('@')
|
||||
},
|
||||
/** @return {import('../types/Mastodon.js').Account} */
|
||||
currentAccount() {
|
||||
return this.$store.getters.currentAccount
|
||||
},
|
||||
/** @return {boolean} */
|
||||
isNotification() {
|
||||
return this.item.type !== undefined
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
|
@ -158,21 +190,22 @@ export default {
|
|||
*/
|
||||
getSinglePostTimeline(e) {
|
||||
// Display internal or external post
|
||||
if (!this.item.local) {
|
||||
if (this.item.type === 'Note') {
|
||||
if (!this.isLocal) {
|
||||
if (this.type === 'Note') {
|
||||
window.open(this.item.id)
|
||||
} else if (this.item.type === 'Announce') {
|
||||
} else if (this.type === 'Announce') {
|
||||
// TODO
|
||||
window.open(this.item.object)
|
||||
} else {
|
||||
logger.warn("Don't know what to do with posts of type " + this.item.type, { post: this.item })
|
||||
logger.warn("Don't know what to do with posts of type " + this.type, { post: this.item })
|
||||
}
|
||||
} else {
|
||||
this.$router.push({
|
||||
name: 'single-post',
|
||||
params: {
|
||||
account: this.item.actor_info.preferredUsername,
|
||||
account: this.item.account.display_name,
|
||||
id: this.item.id,
|
||||
localId: this.item.id.split('/')[this.item.id.split('/').length - 1],
|
||||
localId: this.item.uri.split('/').pop(),
|
||||
type: 'single-post',
|
||||
},
|
||||
})
|
||||
|
@ -188,7 +221,7 @@ export default {
|
|||
boost() {
|
||||
const params = {
|
||||
post: this.item,
|
||||
parentAnnounce: this.parentAnnounce,
|
||||
parentAnnounce: this.reblog,
|
||||
}
|
||||
if (this.isBoosted) {
|
||||
this.$store.dispatch('postUnBoost', params)
|
||||
|
@ -202,7 +235,7 @@ export default {
|
|||
like() {
|
||||
const params = {
|
||||
post: this.item,
|
||||
parentAnnounce: this.parentAnnounce,
|
||||
parentAnnounce: this.reblog,
|
||||
}
|
||||
if (this.isLiked) {
|
||||
this.$store.dispatch('postUnlike', params)
|
||||
|
|
|
@ -24,19 +24,19 @@
|
|||
<div v-if="item" class="user-entry">
|
||||
<div class="entry-content">
|
||||
<div class="user-avatar">
|
||||
<NcAvatar v-if="item.local"
|
||||
<NcAvatar v-if="isLocal"
|
||||
:size="32"
|
||||
:user="item.preferredUsername"
|
||||
:user="item.username"
|
||||
:disable-tooltip="true" />
|
||||
<NcAvatar v-else :url="avatarUrl" />
|
||||
<NcAvatar v-else :url="item.avatar" />
|
||||
</div>
|
||||
<div class="user-details">
|
||||
<router-link v-if="!serverData.public" :to="{ name: 'profile', params: { account: item.local ? item.preferredUsername : item.account }}">
|
||||
<router-link v-if="!serverData.public" :to="{ name: 'profile', params: { account: item.acct }}">
|
||||
<span class="post-author">
|
||||
{{ item.name }}
|
||||
{{ item.display_name }}
|
||||
</span>
|
||||
<span class="user-description">
|
||||
{{ item.account }}
|
||||
{{ item.acct }}
|
||||
</span>
|
||||
</router-link>
|
||||
<a v-else
|
||||
|
@ -44,26 +44,24 @@
|
|||
target="_blank"
|
||||
rel="noreferrer">
|
||||
<span class="post-author">
|
||||
{{ item.name }}
|
||||
{{ item.display_name }}
|
||||
</span>
|
||||
<span class="user-description">
|
||||
{{ item.account }}
|
||||
{{ item.acct }}
|
||||
</span>
|
||||
</a>
|
||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||
<p v-html="item.summary" />
|
||||
<p v-html="item.note" />
|
||||
</div>
|
||||
<FollowButton :account="item.account" :uid="cloudId" />
|
||||
<FollowButton :account="item.acct" :uid="cloudId" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
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'
|
||||
|
||||
export default {
|
||||
name: 'UserEntry',
|
||||
|
@ -72,11 +70,14 @@ export default {
|
|||
NcAvatar,
|
||||
},
|
||||
mixins: [
|
||||
follow,
|
||||
currentUser,
|
||||
],
|
||||
props: {
|
||||
item: { type: Object, default: () => {} },
|
||||
/** @type {import('vue').PropType<import('../types/Mastodon.js').Account>} */
|
||||
item: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
@ -84,14 +85,11 @@ export default {
|
|||
}
|
||||
},
|
||||
computed: {
|
||||
id() {
|
||||
if (this.item.actor_info) {
|
||||
return this.item.actor_info.id
|
||||
}
|
||||
return this.item.id
|
||||
},
|
||||
avatarUrl() {
|
||||
return generateUrl('/apps/social/api/v1/global/actor/avatar?id=' + this.id)
|
||||
/**
|
||||
* @return {boolean}
|
||||
*/
|
||||
isLocal() {
|
||||
return !this.item.acct.includes('@')
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
@ -1,28 +0,0 @@
|
|||
/*
|
||||
* @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
|
||||
*
|
||||
* @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
|
||||
*
|
||||
* @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 { getLoggerBuilder } from '@nextcloud/logger'
|
||||
import { getCurrentUser } from '@nextcloud/auth'
|
||||
|
||||
export default getLoggerBuilder()
|
||||
.setApp('social')
|
||||
.setUid(getCurrentUser().uid)
|
||||
.build()
|
|
@ -27,9 +27,9 @@ import App from './App.vue'
|
|||
import store from './store/index.js'
|
||||
import router from './router.js'
|
||||
import vuetwemoji from 'vue-twemoji'
|
||||
import contenteditableDirective from 'vue-contenteditable-directive'
|
||||
import ClickOutside from 'vue-click-outside'
|
||||
import VTooltip from '@nextcloud/vue/dist/Directives/Tooltip.js'
|
||||
import VueMasonry from 'vue-masonry-css'
|
||||
|
||||
sync(store, router)
|
||||
|
||||
|
@ -49,13 +49,13 @@ Vue.prototype.OCA = OCA
|
|||
|
||||
Vue.directive('ClickOutside', ClickOutside)
|
||||
Vue.directive('Tooltip', VTooltip)
|
||||
Vue.use(contenteditableDirective)
|
||||
Vue.use(vuetwemoji, {
|
||||
baseUrl: OC.linkTo('social', 'img/'), // can set to local folder of emojis. default: https://twemoji.maxcdn.com/
|
||||
extension: '.svg', // .svg, .png
|
||||
className: 'emoji', // custom className for image output
|
||||
size: 'twemoji', // image size
|
||||
})
|
||||
Vue.use(VueMasonry)
|
||||
|
||||
/* eslint-disable-next-line no-new */
|
||||
new Vue({
|
||||
|
|
|
@ -36,7 +36,7 @@ export default {
|
|||
return (this.uid.indexOf('@') === -1) ? this.uid + '@' + this.hostname : this.uid
|
||||
},
|
||||
|
||||
/** @return detailed information about an account (account must be loaded in the store first) */
|
||||
/** @return {import('../types/Mastodon.js').Account} detailed information about an account (account must be loaded in the store first) */
|
||||
accountInfo() {
|
||||
return this.$store.getters.getAccount(this.profileAccount)
|
||||
},
|
||||
|
@ -44,9 +44,20 @@ export default {
|
|||
/**
|
||||
* Somewhat duplicate with accountInfo(), but needed (for some reason) to avoid glitches
|
||||
* where components would first show "user not found" before display an account's account info
|
||||
*
|
||||
* @return {boolean}
|
||||
*/
|
||||
accountLoaded() {
|
||||
return this.$store.getters.accountLoaded(this.profileAccount)
|
||||
return this.$store.getters.accountLoaded(this.profileAccount) !== undefined
|
||||
},
|
||||
|
||||
/** @return {boolean} */
|
||||
isLocal() {
|
||||
return !this.accountInfo.acct.includes('@')
|
||||
},
|
||||
/** @return {import('../types/Mastodon.js').Relationship} */
|
||||
relationship() {
|
||||
return this.$store.getters.getRelationshipWith(this.accountInfo.id)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
/*
|
||||
/**
|
||||
* @copyright Copyright (c) 2018 Julius Härtl <jus@bitgrid.net>
|
||||
*
|
||||
* @author Julius Härtl <jus@bitgrid.net>
|
||||
|
@ -20,14 +20,17 @@
|
|||
*
|
||||
*/
|
||||
|
||||
import { getCurrentUser } from '@nextcloud/auth'
|
||||
|
||||
import serverData from './serverData.js'
|
||||
|
||||
export default {
|
||||
mixins: [
|
||||
serverData,
|
||||
],
|
||||
computed: {
|
||||
currentUser() {
|
||||
return OC.getCurrentUser()
|
||||
return getCurrentUser()
|
||||
},
|
||||
socialId() {
|
||||
return '@' + this.cloudId
|
||||
|
|
|
@ -1,71 +0,0 @@
|
|||
/*
|
||||
* @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 { generateUrl } from '@nextcloud/router'
|
||||
|
||||
class FollowException {
|
||||
|
||||
}
|
||||
|
||||
class UnfollowException {
|
||||
|
||||
}
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
followLoading: false,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
follow() {
|
||||
this.followLoading = true
|
||||
return axios.put(generateUrl('/apps/social/api/v1/current/follow?account=' + this.item.account)).then((response) => {
|
||||
this.followLoading = false
|
||||
if (response.data.status === -1) {
|
||||
throw new FollowException()
|
||||
}
|
||||
this.item.details.following = true
|
||||
}).catch((error) => {
|
||||
this.followLoading = false
|
||||
OC.Notification.showTemporary(`Failed to follow user ${this.item.account}`)
|
||||
console.error(`Failed to follow user ${this.item.account}`, error.response.data)
|
||||
})
|
||||
|
||||
},
|
||||
unfollow() {
|
||||
this.followLoading = true
|
||||
return axios.delete(generateUrl('/apps/social/api/v1/current/follow?account=' + this.item.account)).then((response) => {
|
||||
this.followLoading = false
|
||||
if (response.data.status === -1) {
|
||||
throw new UnfollowException()
|
||||
}
|
||||
this.item.details.following = false
|
||||
}).catch((error) => {
|
||||
this.followLoading = false
|
||||
OC.Notification.showTemporary(`Failed to unfollow user ${this.item.account}`)
|
||||
console.error(`Failed to unfollow user ${this.item.account}`, error.response.data)
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
/*
|
||||
/**
|
||||
* @copyright Copyright (c) 2019 Julius Härtl <jus@bitgrid.net>
|
||||
*
|
||||
* @author Julius Härtl <jus@bitgrid.net>
|
||||
|
|
|
@ -39,7 +39,7 @@
|
|||
export default {
|
||||
computed: {
|
||||
/**
|
||||
* @return {Partial<ServerData>} Returns the serverData object
|
||||
* @return {ServerData} Returns the serverData object
|
||||
*/
|
||||
serverData() {
|
||||
if (!this.$store) {
|
||||
|
@ -47,6 +47,9 @@ export default {
|
|||
}
|
||||
return this.$store.getters.getServerData
|
||||
},
|
||||
/**
|
||||
* @return {string}
|
||||
*/
|
||||
hostname() {
|
||||
const url = document.createElement('a')
|
||||
url.setAttribute('href', this.serverData.cloudAddress)
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
/*
|
||||
/**
|
||||
* @copyright Copyright (c) 2019 Julius Härtl <jus@bitgrid.net>
|
||||
*
|
||||
* @author Julius Härtl <jus@bitgrid.net>
|
||||
|
|
|
@ -59,14 +59,6 @@ export default new Router({
|
|||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/:index(index.php/)?apps/social/@:account/:localId',
|
||||
components: {
|
||||
default: TimelineSinglePost,
|
||||
},
|
||||
props: true,
|
||||
name: 'single-post',
|
||||
},
|
||||
{
|
||||
path: '/:index(index.php/)?apps/social/@:account',
|
||||
components: {
|
||||
|
@ -99,6 +91,14 @@ export default new Router({
|
|||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/:index(index.php/)?apps/social/@:account/:localId',
|
||||
components: {
|
||||
default: TimelineSinglePost,
|
||||
},
|
||||
props: true,
|
||||
name: 'single-post',
|
||||
},
|
||||
{
|
||||
path: '/:index(index.php/)?apps/social/ostatus/follow',
|
||||
components: {
|
||||
|
|
|
@ -23,162 +23,234 @@
|
|||
import axios from '@nextcloud/axios'
|
||||
import { set } from 'vue'
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
import { showError } from '@nextcloud/dialogs'
|
||||
import logger from '../services/logger.js'
|
||||
|
||||
const state = {
|
||||
currentAccount: {},
|
||||
currentAccount: '',
|
||||
/** @type {Object<string, import('../types/Mastodon.js').Account>} */
|
||||
accounts: {},
|
||||
/** @type {Object<string, string[]>} */
|
||||
accountsFollowers: {},
|
||||
/** @type {Object<string, string[]>} */
|
||||
accountsFollowings: {},
|
||||
/** @type {Object<string, Partial<import('../types/Mastodon.js').Relationship>>} */
|
||||
accountsRelationships: {},
|
||||
/** @type {Object<string, string>} */
|
||||
accountIdMap: {},
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {typeof state} state
|
||||
* @param {object} payload
|
||||
* @param {string} payload.actorId
|
||||
* @param {import('../types/Mastodon').Account} payload.data
|
||||
*/
|
||||
const addAccount = (state, { actorId, data }) => {
|
||||
set(state.accounts, actorId, Object.assign({
|
||||
followersList: [],
|
||||
followingList: [],
|
||||
details: {
|
||||
following: false,
|
||||
follower: false,
|
||||
},
|
||||
}, state.accounts[actorId], data))
|
||||
set(state.accountIdMap, data.account, data.id)
|
||||
set(state.accounts, actorId, { ...state.accounts[actorId], ...data })
|
||||
set(state.accountsFollowers, actorId, [])
|
||||
set(state.accountsFollowings, actorId, [])
|
||||
const accountId = (data.acct.indexOf('@') === -1) ? data.acct + '@' + new URL(data.url).hostname : data.acct
|
||||
set(state.accountIdMap, accountId, data.url)
|
||||
}
|
||||
const _getActorIdForAccount = (account) => state.accountIdMap[account]
|
||||
|
||||
/** @type {import('vuex').MutationTree<state, any>} */
|
||||
const mutations = {
|
||||
/**
|
||||
* @param state
|
||||
* @param {string} account
|
||||
*/
|
||||
setCurrentAccount(state, account) {
|
||||
state.currentAccount = account
|
||||
},
|
||||
/**
|
||||
* @param state
|
||||
* @param {object} payload
|
||||
* @param {string} payload.actorId
|
||||
* @param {import('../types/Mastodon').Account} payload.data
|
||||
*/
|
||||
addAccount(state, { actorId, data }) {
|
||||
addAccount(state, { actorId, data })
|
||||
},
|
||||
/**
|
||||
* @param state
|
||||
* @param {object} payload
|
||||
* @param {string} payload.actorId
|
||||
* @param {import('../types/Mastodon').Relationship} payload.data
|
||||
*/
|
||||
addRelationship(state, { actorId, data }) {
|
||||
set(state.accountsRelationships, actorId, data)
|
||||
},
|
||||
/**
|
||||
* @param state
|
||||
* @param {object} root
|
||||
* @param {string} root.account
|
||||
* @param {import('../types/Mastodon.js').Account[]} root.data
|
||||
*/
|
||||
addFollowers(state, { account, data }) {
|
||||
const users = []
|
||||
for (const index in data) {
|
||||
const actor = data[index].actor_info
|
||||
if (typeof actor !== 'undefined' && account !== actor.account) {
|
||||
users.push(actor.id)
|
||||
addAccount(state, {
|
||||
actorId: actor.id,
|
||||
data: actor,
|
||||
})
|
||||
}
|
||||
for (const actor of data) {
|
||||
users.push(actor.url)
|
||||
addAccount(state, {
|
||||
actorId: actor.url,
|
||||
data: actor,
|
||||
})
|
||||
}
|
||||
set(state.accounts[_getActorIdForAccount(account)], 'followersList', users)
|
||||
set(state.accountsFollowers, _getActorIdForAccount(account), users)
|
||||
},
|
||||
/**
|
||||
* @param state
|
||||
* @param {object} root
|
||||
* @param {string} root.account
|
||||
* @param {import('../types/Mastodon.js').Account[]} root.data
|
||||
*/
|
||||
addFollowing(state, { account, data }) {
|
||||
const users = []
|
||||
for (const index in data) {
|
||||
const actor = data[index].actor_info
|
||||
if (typeof actor !== 'undefined' && account !== actor.account) {
|
||||
users.push(actor.id)
|
||||
addAccount(state, {
|
||||
actorId: actor.id,
|
||||
data: actor,
|
||||
})
|
||||
}
|
||||
for (const actor of data) {
|
||||
users.push(actor.url)
|
||||
addAccount(state, {
|
||||
actorId: actor.url,
|
||||
data: actor,
|
||||
})
|
||||
}
|
||||
set(state.accounts[_getActorIdForAccount(account)], 'followingList', users)
|
||||
set(state.accountsFollowings, _getActorIdForAccount(account), users)
|
||||
},
|
||||
followAccount(state, accountToFollow) {
|
||||
set(state.accounts[_getActorIdForAccount(accountToFollow)].details, 'following', true)
|
||||
state.accountsFollowings[_getActorIdForAccount(accountToFollow)].push(accountToFollow)
|
||||
set(state.accountsRelationships[state.accounts[_getActorIdForAccount(accountToFollow)].id], 'following', true)
|
||||
},
|
||||
unfollowAccount(state, accountToUnfollow) {
|
||||
set(state.accounts[_getActorIdForAccount(accountToUnfollow)].details, 'following', false)
|
||||
const followingList = state.accountsFollowings[_getActorIdForAccount(accountToUnfollow)]
|
||||
followingList.splice(followingList.indexOf(accountToUnfollow), 1)
|
||||
set(state.accountsRelationships[state.accounts[_getActorIdForAccount(accountToUnfollow)].id], 'following', false)
|
||||
},
|
||||
}
|
||||
|
||||
/** @type {import('vuex').GetterTree<state, any>} */
|
||||
const getters = {
|
||||
getAllAccounts(state) {
|
||||
return (account) => { return state.accounts }
|
||||
return () => { return state.accounts }
|
||||
},
|
||||
getAccount(state, getters) {
|
||||
return (account) => {
|
||||
return (/** @type {string} */ account) => {
|
||||
return state.accounts[_getActorIdForAccount(account)]
|
||||
}
|
||||
},
|
||||
getRelationshipWith(state, getters) {
|
||||
return (/** @type {string} */ accountId) => {
|
||||
return state.accountsRelationships[accountId]
|
||||
}
|
||||
},
|
||||
currentAccount(state, getters) {
|
||||
return getters.getAccount(state.currentAccount)
|
||||
},
|
||||
accountFollowing(state) {
|
||||
return (account, isFollowing) => _getActorIdForAccount(isFollowing) in state.accounts[_getActorIdForAccount(account)]
|
||||
return (/** @type {string} */ account, /** @type {boolean} */ isFollowing) => _getActorIdForAccount(isFollowing) in state.accounts[_getActorIdForAccount(account)]
|
||||
},
|
||||
accountLoaded(state) {
|
||||
return (account) => state.accounts[_getActorIdForAccount(account)]
|
||||
return (/** @type {string} */ account) => state.accounts[_getActorIdForAccount(account)]
|
||||
},
|
||||
getAccountFollowers(state) {
|
||||
return (id) => state.accounts[_getActorIdForAccount(id)].followersList.map((actorId) => state.accounts[actorId])
|
||||
return (/** @type {string} */ id) => state.accountsFollowers[_getActorIdForAccount(id)].map((actorId) => state.accounts[actorId])
|
||||
},
|
||||
getAccountFollowing(state) {
|
||||
return (id) => state.accounts[_getActorIdForAccount(id)].followingList.map((actorId) => state.accounts[actorId])
|
||||
return (/** @type {string} */ id) => state.accountsFollowings[_getActorIdForAccount(id)].map((actorId) => state.accounts[actorId])
|
||||
},
|
||||
getActorIdForAccount() {
|
||||
return _getActorIdForAccount
|
||||
},
|
||||
isFollowingUser(state) {
|
||||
return (followingAccount) => {
|
||||
const account = state.accounts[_getActorIdForAccount(followingAccount)]
|
||||
return account && account.details ? account.details.following : false
|
||||
}
|
||||
return (/** @type {string} */ followingAccount) => state.accountsRelationships[_getActorIdForAccount(followingAccount)]?.following || false
|
||||
},
|
||||
}
|
||||
|
||||
/** @type {import('vuex').ActionTree<state, any>} */
|
||||
const actions = {
|
||||
fetchAccountInfo(context, account) {
|
||||
return axios.get(generateUrl(`apps/social/api/v1/global/account/info?account=${account}`)).then((response) => {
|
||||
context.commit('addAccount', { actorId: response.data.result.account.id, data: response.data.result.account })
|
||||
return response.data.result.account
|
||||
}).catch(() => {
|
||||
OC.Notification.showTemporary(`Failed to load account details ${account}`)
|
||||
})
|
||||
async fetchAccountInfo(context, account) {
|
||||
try {
|
||||
const response = await axios.get(generateUrl(`apps/social/api/v1/global/account/info?account=${account}`))
|
||||
context.commit('addAccount', { actorId: response.data.url, data: response.data })
|
||||
return response.data
|
||||
} catch (error) {
|
||||
logger.error('Failed to load local account details', { error })
|
||||
showError(`Failed to load local account details ${account}`)
|
||||
}
|
||||
},
|
||||
fetchPublicAccountInfo(context, uid) {
|
||||
return axios.get(generateUrl(`apps/social/api/v1/account/${uid}/info`)).then((response) => {
|
||||
context.commit('addAccount', { actorId: response.data.result.account.id, data: response.data.result.account })
|
||||
return response.data.result.account
|
||||
}).catch(() => {
|
||||
OC.Notification.showTemporary(`Failed to load account details ${uid}`)
|
||||
})
|
||||
async fetchAccountRelationshipInfo(context, ids) {
|
||||
try {
|
||||
const response = await axios.get(generateUrl('apps/social/api/v1/accounts/relationships'), { params: { id: ids } })
|
||||
response.data.forEach(account => context.commit('addRelationship', { actorId: account.id, data: account }))
|
||||
return response.data
|
||||
} catch (error) {
|
||||
logger.error('Failed to load relationship info', { error })
|
||||
showError('Failed to load relationship info')
|
||||
}
|
||||
},
|
||||
async fetchPublicAccountInfo(context, uid) {
|
||||
try {
|
||||
const response = await axios.get(generateUrl(`apps/social/api/v1/account/${uid}/info`))
|
||||
context.commit('addAccount', { actorId: response.data.url, data: response.data })
|
||||
return response.data
|
||||
} catch (error) {
|
||||
logger.error('Failed to load public account details', { error })
|
||||
showError(`Failed to load public account details ${uid}`)
|
||||
}
|
||||
},
|
||||
fetchCurrentAccountInfo({ commit, dispatch }, account) {
|
||||
commit('setCurrentAccount', account)
|
||||
dispatch('fetchAccountInfo', account)
|
||||
},
|
||||
followAccount(context, { currentAccount, accountToFollow }) {
|
||||
return axios.put(generateUrl('/apps/social/api/v1/current/follow?account=' + accountToFollow)).then((response) => {
|
||||
async followAccount(context, { currentAccount, accountToFollow }) {
|
||||
try {
|
||||
const response = await axios.put(generateUrl('/apps/social/api/v1/current/follow?account=' + accountToFollow))
|
||||
if (response.data.status === -1) {
|
||||
return Promise.reject(response)
|
||||
}
|
||||
context.commit('followAccount', accountToFollow)
|
||||
return Promise.resolve(response)
|
||||
}).catch((error) => {
|
||||
OC.Notification.showTemporary(`Failed to follow user ${accountToFollow}`)
|
||||
console.error(`Failed to follow user ${accountToFollow}`, error)
|
||||
})
|
||||
|
||||
return response
|
||||
} catch (error) {
|
||||
showError(`Failed to follow user ${accountToFollow}`)
|
||||
logger.error(`Failed to follow user ${accountToFollow}`, { error })
|
||||
}
|
||||
},
|
||||
unfollowAccount(context, { currentAccount, accountToUnfollow }) {
|
||||
return axios.delete(generateUrl('/apps/social/api/v1/current/follow?account=' + accountToUnfollow)).then((response) => {
|
||||
async unfollowAccount(context, { currentAccount, accountToUnfollow }) {
|
||||
try {
|
||||
const response = await axios.delete(generateUrl('/apps/social/api/v1/current/follow?account=' + accountToUnfollow))
|
||||
if (response.data.status === -1) {
|
||||
return Promise.reject(response)
|
||||
}
|
||||
context.commit('unfollowAccount', accountToUnfollow)
|
||||
return Promise.resolve(response)
|
||||
}).catch((error) => {
|
||||
OC.Notification.showTemporary(`Failed to unfollow user ${accountToUnfollow}`)
|
||||
console.error(`Failed to unfollow user ${accountToUnfollow}`, error.response.data)
|
||||
return Promise.reject(error.response.data)
|
||||
})
|
||||
return response
|
||||
} catch (error) {
|
||||
showError(`Failed to unfollow user ${accountToUnfollow}`)
|
||||
logger.error(`Failed to unfollow user ${accountToUnfollow}`, { error })
|
||||
return error
|
||||
}
|
||||
},
|
||||
fetchAccountFollowers(context, account) {
|
||||
async fetchAccountFollowers(context, account) {
|
||||
// TODO: fetching followers/following information of remotes is currently not supported
|
||||
const parts = account.split('@')
|
||||
const uid = (parts.length === 2 ? parts[0] : account)
|
||||
axios.get(generateUrl(`apps/social/api/v1/account/${uid}/followers`)).then((response) => {
|
||||
context.commit('addFollowers', { account, data: response.data.result })
|
||||
})
|
||||
try {
|
||||
const response = await axios.get(generateUrl(`apps/social/api/v1/accounts/${uid}/followers`))
|
||||
context.commit('addFollowers', { account, data: response.data })
|
||||
} catch (error) {
|
||||
showError('Failed to fetch followers list')
|
||||
logger.error(`Failed to fetch followers list for user ${account}`, { error })
|
||||
}
|
||||
},
|
||||
fetchAccountFollowing(context, account) {
|
||||
async fetchAccountFollowing(context, account) {
|
||||
// TODO: fetching followers/following information of remotes is currently not supported
|
||||
const parts = account.split('@')
|
||||
const uid = (parts.length === 2 ? parts[0] : account)
|
||||
axios.get(generateUrl(`apps/social/api/v1/account/${uid}/following`)).then((response) => {
|
||||
context.commit('addFollowing', { account, data: response.data.result })
|
||||
})
|
||||
try {
|
||||
const response = await axios.get(generateUrl(`apps/social/api/v1/accounts/${uid}/following`))
|
||||
context.commit('addFollowing', { account, data: response.data })
|
||||
} catch (error) {
|
||||
showError('Failed to fetch following list')
|
||||
logger.error(`Failed to fetch following list for user ${account}`, { error })
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -23,103 +23,157 @@
|
|||
*
|
||||
*/
|
||||
|
||||
import logger from '../services/logger.js'
|
||||
import axios from '@nextcloud/axios'
|
||||
import Vue from 'vue'
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
|
||||
/**
|
||||
* @property {object} timeline - The posts' collection
|
||||
* @property {number} since - Time (EPOCH) of the most recent post
|
||||
* @property {string} type - Timeline's type: 'home', 'single-post',...
|
||||
* @property {object} params - Timeline's parameters
|
||||
* @property {string} account -
|
||||
*/
|
||||
import axios from '@nextcloud/axios'
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
import { showError } from '@nextcloud/dialogs'
|
||||
|
||||
import logger from '../services/logger.js'
|
||||
|
||||
const state = {
|
||||
/**
|
||||
* @type {Object<string, import('../types/Mastodon.js').Status>} timeline - The posts' collection
|
||||
*/
|
||||
timeline: {},
|
||||
since: Math.floor(Date.now() / 1000) + 1,
|
||||
/**
|
||||
* @type {string} type - Timeline's type: 'home', 'single-post',...
|
||||
*/
|
||||
type: 'home',
|
||||
/**
|
||||
* @namespace params
|
||||
* @property {string} account ???
|
||||
* @property {string} id
|
||||
* @property {string} localId
|
||||
* @property {string} type ???
|
||||
* @type {object} params - Timeline's parameters
|
||||
* @property {string} params.account ???
|
||||
* @property {string} params.id
|
||||
* @property {string} params.localId
|
||||
* @property {string} params.type ???
|
||||
*/
|
||||
params: {},
|
||||
/**
|
||||
* @type {string} account -
|
||||
*/
|
||||
account: '',
|
||||
/* Tells whether the composer should be displayed or not.
|
||||
/**
|
||||
* Tells whether the composer should be displayed or not.
|
||||
* It's up to the view to honor this status or not.
|
||||
*
|
||||
* @member {boolean}
|
||||
*/
|
||||
composerDisplayStatus: false,
|
||||
}
|
||||
|
||||
/** @type {import('vuex').MutationTree<state>} */
|
||||
const mutations = {
|
||||
/**
|
||||
* @param state
|
||||
* @param {import('../types/Mastodon.js').Status[]} data
|
||||
*/
|
||||
addToTimeline(state, data) {
|
||||
for (const item in data) {
|
||||
state.since = data[item].publishedTime
|
||||
Vue.set(state.timeline, data[item].id, data[item])
|
||||
// TODO: fix to handle ancestors
|
||||
if (data.descendants) {
|
||||
data = data.descendants
|
||||
}
|
||||
data.forEach((post) => Vue.set(state.timeline, post.id, post))
|
||||
},
|
||||
/**
|
||||
* @param state
|
||||
* @param {import('../types/Mastodon.js').Status} post
|
||||
*/
|
||||
removePost(state, post) {
|
||||
Vue.delete(state.timeline, post.id)
|
||||
},
|
||||
resetTimeline(state) {
|
||||
state.timeline = {}
|
||||
state.since = Math.floor(Date.now() / 1000) + 1
|
||||
},
|
||||
/**
|
||||
* @param state
|
||||
* @param {string} type
|
||||
*/
|
||||
setTimelineType(state, type) {
|
||||
state.type = type
|
||||
},
|
||||
setTimelineParams(state, params) {
|
||||
state.params = params
|
||||
},
|
||||
/**
|
||||
* @param state
|
||||
* @param {boolean} status
|
||||
*/
|
||||
setComposerDisplayStatus(state, status) {
|
||||
state.composerDisplayStatus = status
|
||||
},
|
||||
/**
|
||||
* @param state
|
||||
* @param {string} account
|
||||
*/
|
||||
setAccount(state, account) {
|
||||
state.account = account
|
||||
},
|
||||
/**
|
||||
* @param state
|
||||
* @param {object} root0
|
||||
* @param {import('../types/Mastodon.js').Status} root0.post
|
||||
* @param {object} root0.parentAnnounce
|
||||
*/
|
||||
likePost(state, { post, parentAnnounce }) {
|
||||
if (typeof state.timeline[post.id] !== 'undefined') {
|
||||
Vue.set(state.timeline[post.id].action.values, 'liked', true)
|
||||
Vue.set(state.timeline[post.id], 'favourited', true)
|
||||
}
|
||||
if (typeof parentAnnounce.id !== 'undefined') {
|
||||
Vue.set(state.timeline[parentAnnounce.id].cache[parentAnnounce.object].object.action.values, 'liked', true)
|
||||
Vue.set(state.timeline[parentAnnounce.id].cache[parentAnnounce.object], 'favourited', true)
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @param state
|
||||
* @param {object} root0
|
||||
* @param {import('../types/Mastodon.js').Status} root0.post
|
||||
* @param {object} root0.parentAnnounce
|
||||
*/
|
||||
unlikePost(state, { post, parentAnnounce }) {
|
||||
if (typeof state.timeline[post.id] !== 'undefined') {
|
||||
Vue.set(state.timeline[post.id].action.values, 'liked', false)
|
||||
Vue.set(state.timeline[post.id], 'favourited', false)
|
||||
}
|
||||
if (typeof parentAnnounce.id !== 'undefined') {
|
||||
Vue.set(state.timeline[parentAnnounce.id].cache[parentAnnounce.object].object.action.values, 'liked', false)
|
||||
Vue.set(state.timeline[parentAnnounce.id].cache[parentAnnounce.object].object, 'favourited', false)
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @param state
|
||||
* @param {object} root0
|
||||
* @param {import('../types/Mastodon.js').Status} root0.post
|
||||
* @param {object} root0.parentAnnounce
|
||||
*/
|
||||
boostPost(state, { post, parentAnnounce }) {
|
||||
if (typeof state.timeline[post.id] !== 'undefined') {
|
||||
Vue.set(state.timeline[post.id].action.values, 'boosted', true)
|
||||
Vue.set(state.timeline[post.id], 'reblogged', true)
|
||||
}
|
||||
if (typeof parentAnnounce.id !== 'undefined') {
|
||||
Vue.set(state.timeline[parentAnnounce.id].cache[parentAnnounce.object].object.action.values, 'boosted', true)
|
||||
Vue.set(state.timeline[parentAnnounce.id].cache[parentAnnounce.object].object, 'reblogged', true)
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @param state
|
||||
* @param {object} root0
|
||||
* @param {import('../types/Mastodon.js').Status} root0.post
|
||||
* @param {object} root0.parentAnnounce
|
||||
*/
|
||||
unboostPost(state, { post, parentAnnounce }) {
|
||||
if (typeof state.timeline[post.id] !== 'undefined') {
|
||||
Vue.set(state.timeline[post.id].action.values, 'boosted', false)
|
||||
Vue.set(state.timeline[post.id], 'reblogged', false)
|
||||
}
|
||||
if (typeof parentAnnounce.id !== 'undefined') {
|
||||
Vue.set(state.timeline[parentAnnounce.id].cache[parentAnnounce.object].object.action.values, 'boosted', false)
|
||||
Vue.set(state.timeline[parentAnnounce.id].cache[parentAnnounce.object].object, 'reblogged', false)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
/** @type {import('vuex').GetterTree<state, any>} */
|
||||
const getters = {
|
||||
getComposerDisplayStatus(state) {
|
||||
return state.composerDisplayStatus
|
||||
},
|
||||
getTimeline(state) {
|
||||
return Object.values(state.timeline).sort(function(a, b) {
|
||||
return b.publishedTime - a.publishedTime
|
||||
return new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
|
||||
})
|
||||
},
|
||||
getPostFromTimeline(state) {
|
||||
|
@ -132,6 +186,8 @@ const getters = {
|
|||
}
|
||||
},
|
||||
}
|
||||
|
||||
/** @type {import('vuex').ActionTree<state, any>} */
|
||||
const actions = {
|
||||
changeTimelineType(context, { type, params }) {
|
||||
context.commit('resetTimeline')
|
||||
|
@ -144,108 +200,183 @@ const actions = {
|
|||
context.commit('setTimelineType', 'account')
|
||||
context.commit('setAccount', account)
|
||||
},
|
||||
async post(context, post) {
|
||||
/**
|
||||
* @param context
|
||||
* @param {File} file
|
||||
*/
|
||||
async createMedia(context, file) {
|
||||
try {
|
||||
const { data } = await axios.post(generateUrl('apps/social/api/v1/post'), post, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
})
|
||||
logger.info('Post created with token ' + data.result.token)
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
const { data } = await axios.post(
|
||||
generateUrl('apps/social/api/v1/media'),
|
||||
formData,
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
}
|
||||
)
|
||||
logger.info('Media created with id ' + data.id)
|
||||
return data
|
||||
} catch (error) {
|
||||
OC.Notification.showTemporary('Failed to create a post')
|
||||
logger.error('Failed to create a post', { error: error.response })
|
||||
showError('Failed to create a media')
|
||||
logger.error('Failed to create a media', { error })
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @param context
|
||||
* @param {import('../types/Mastodon.js').Status} post
|
||||
*/
|
||||
async post(context, post) {
|
||||
try {
|
||||
const { data } = await axios.post(generateUrl('apps/social/api/v1/statuses'), post)
|
||||
logger.info('Post created with token ' + data.id)
|
||||
} catch (error) {
|
||||
showError('Failed to create a post')
|
||||
logger.error('Failed to create a post', { error })
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @param context
|
||||
* @param {import('../types/Mastodon.js').Status} post
|
||||
*/
|
||||
postDelete(context, post) {
|
||||
return axios.delete(generateUrl(`apps/social/api/v1/post?id=${post.id}`)).then((response) => {
|
||||
return axios.delete(generateUrl(`apps/social/api/v1/post?id=${post.uri}`)).then((response) => {
|
||||
context.commit('removePost', post)
|
||||
logger.info('Post deleted with token ' + response.data.result.token)
|
||||
}).catch((error) => {
|
||||
OC.Notification.showTemporary('Failed to delete the post')
|
||||
showError('Failed to delete the post')
|
||||
logger.error('Failed to delete the post', { error })
|
||||
})
|
||||
},
|
||||
/**
|
||||
* @param context
|
||||
* @param {object} root0
|
||||
* @param {import('../types/Mastodon.js').Status} root0.post
|
||||
* @param {object} root0.parentAnnounce
|
||||
*/
|
||||
postLike(context, { post, parentAnnounce }) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios.post(generateUrl(`apps/social/api/v1/post/like?postId=${post.id}`)).then((response) => {
|
||||
axios.post(generateUrl(`apps/social/api/v1/post/like?postId=${post.uri}`)).then((response) => {
|
||||
context.commit('likePost', { post, parentAnnounce })
|
||||
resolve(response)
|
||||
}).catch((error) => {
|
||||
OC.Notification.showTemporary('Failed to like post')
|
||||
logger.error('Failed to like post', { error: error.response })
|
||||
showError('Failed to like post')
|
||||
logger.error('Failed to like post', { error })
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
},
|
||||
/**
|
||||
* @param context
|
||||
* @param {object} root0
|
||||
* @param {import('../types/Mastodon.js').Status} root0.post
|
||||
* @param {object} root0.parentAnnounce
|
||||
*/
|
||||
postUnlike(context, { post, parentAnnounce }) {
|
||||
return axios.delete(generateUrl(`apps/social/api/v1/post/like?postId=${post.id}`)).then((response) => {
|
||||
return axios.delete(generateUrl(`apps/social/api/v1/post/like?postId=${post.uri}`)).then((response) => {
|
||||
context.commit('unlikePost', { post, parentAnnounce })
|
||||
// Remove post from list if we are in the 'liked' timeline
|
||||
if (state.type === 'liked') {
|
||||
context.commit('removePost', post)
|
||||
}
|
||||
}).catch((error) => {
|
||||
OC.Notification.showTemporary('Failed to unlike post')
|
||||
showError('Failed to unlike post')
|
||||
logger.error('Failed to unlike post', { error })
|
||||
})
|
||||
},
|
||||
/**
|
||||
* @param context
|
||||
* @param {object} root0
|
||||
* @param {import('../types/Mastodon.js').Status} root0.post
|
||||
* @param {object} root0.parentAnnounce
|
||||
*/
|
||||
postBoost(context, { post, parentAnnounce }) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios.post(generateUrl(`apps/social/api/v1/post/boost?postId=${post.id}`)).then((response) => {
|
||||
axios.post(generateUrl(`apps/social/api/v1/post/boost?postId=${post.uri}`)).then((response) => {
|
||||
context.commit('boostPost', { post, parentAnnounce })
|
||||
logger.info('Post boosted with token ' + response.data.result.token)
|
||||
resolve(response)
|
||||
}).catch((error) => {
|
||||
OC.Notification.showTemporary('Failed to create a boost post')
|
||||
logger.error('Failed to create a boost post', { error: error.response })
|
||||
showError('Failed to create a boost post')
|
||||
logger.error('Failed to create a boost post', { error })
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
},
|
||||
/**
|
||||
* @param context
|
||||
* @param {object} root0
|
||||
* @param {import('../types/Mastodon.js').Status} root0.post
|
||||
* @param {object} root0.parentAnnounce
|
||||
*/
|
||||
postUnBoost(context, { post, parentAnnounce }) {
|
||||
return axios.delete(generateUrl(`apps/social/api/v1/post/boost?postId=${post.id}`)).then((response) => {
|
||||
return axios.delete(generateUrl(`apps/social/api/v1/post/boost?postId=${post.uri}`)).then((response) => {
|
||||
context.commit('unboostPost', { post, parentAnnounce })
|
||||
logger.info('Boost deleted with token ' + response.data.result.token)
|
||||
}).catch((error) => {
|
||||
OC.Notification.showTemporary('Failed to delete the boost')
|
||||
showError('Failed to delete the boost')
|
||||
logger.error('Failed to delete the boost', { error })
|
||||
})
|
||||
},
|
||||
refreshTimeline(context) {
|
||||
return this.dispatch('fetchTimeline', { sinceTimestamp: Math.floor(Date.now() / 1000) + 1 })
|
||||
return this.dispatch('fetchTimeline')
|
||||
},
|
||||
fetchTimeline(context, { sinceTimestamp }) {
|
||||
|
||||
if (typeof sinceTimestamp === 'undefined') {
|
||||
sinceTimestamp = state.since - 1
|
||||
/**
|
||||
*
|
||||
* @param {object} context
|
||||
* @param {object} params - see https://docs.joinmastodon.org/methods/timelines
|
||||
* @param {number} [params.since_id] - Fetch results newer than ID
|
||||
* @param {number} [params.max_id] - Fetch results older than ID
|
||||
* @param {number} [params.min_id] - Fetch results immediately newer than ID
|
||||
* @param {number} [params.limit] - Maximum number of results to return. Defaults to 20 statuses. Max 40 statuses
|
||||
* @param {boolean} [params.local] - Show only local statuses? Defaults to false.
|
||||
* @return {Promise<object[]>}
|
||||
*/
|
||||
async fetchTimeline(context, params = {}) {
|
||||
if (params.limit === undefined) {
|
||||
params.limit = 15
|
||||
}
|
||||
|
||||
// Compute URl to get the data
|
||||
// Compute URL to get the data
|
||||
let url = ''
|
||||
if (state.type === 'account') {
|
||||
url = generateUrl(`apps/social/api/v1/account/${state.account}/stream?limit=25&since=` + sinceTimestamp)
|
||||
} else if (state.type === 'tags') {
|
||||
url = generateUrl(`apps/social/api/v1/stream/tag/${state.params.tag}?limit=25&since=` + sinceTimestamp)
|
||||
} else if (state.type === 'single-post') {
|
||||
url = generateUrl(`apps/social/local/v1/post/replies?id=${state.params.id}&limit=5&since=` + sinceTimestamp)
|
||||
} else {
|
||||
url = generateUrl(`apps/social/api/v1/stream/${state.type}?limit=25&since=` + sinceTimestamp)
|
||||
switch (state.type) {
|
||||
case 'account':
|
||||
url = generateUrl(`apps/social/api/v1/accounts/${state.account}/statuses`)
|
||||
break
|
||||
case 'tags':
|
||||
url = generateUrl(`apps/social/api/v1/timelines/tag/${state.params.tag}`)
|
||||
break
|
||||
case 'single-post':
|
||||
url = generateUrl(`apps/social/api/v1/statuses/${state.params.localId}/context`)
|
||||
break
|
||||
case 'timeline':
|
||||
url = generateUrl('apps/social/api/v1/timelines/public')
|
||||
params.local = true
|
||||
break
|
||||
case 'federated':
|
||||
url = generateUrl('apps/social/api/v1/timelines/public')
|
||||
break
|
||||
case 'notifications':
|
||||
url = generateUrl('apps/social/api/v1/notifications')
|
||||
break
|
||||
default:
|
||||
url = generateUrl(`apps/social/api/v1/timelines/${state.type}`)
|
||||
}
|
||||
|
||||
// Get the data and add them to the timeline
|
||||
return axios.get(url).then((response) => {
|
||||
const response = await axios.get(url, { params })
|
||||
|
||||
if (response.status === -1) {
|
||||
throw response.message
|
||||
}
|
||||
// Add results to timeline
|
||||
context.commit('addToTimeline', response.data)
|
||||
|
||||
// Add results to timeline
|
||||
context.commit('addToTimeline', response.data.result)
|
||||
|
||||
return response.data
|
||||
})
|
||||
return response.data
|
||||
},
|
||||
/**
|
||||
* @param context
|
||||
* @param {import('../types/Mastodon.js').Status[]} data
|
||||
*/
|
||||
addToTimeline(context, data) {
|
||||
context.commit('addToTimeline', data)
|
||||
},
|
||||
|
|
|
@ -0,0 +1,75 @@
|
|||
/**
|
||||
* @copyright Copyright (c) 2023 Louis Chmn <louis@chmn.me>
|
||||
*
|
||||
* @author Louis Chmn <louis@chmn.me>
|
||||
*
|
||||
* @license AGPL-3.0-or-later
|
||||
*
|
||||
* 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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef APObject - https://www.w3.org/TR/activitystreams-vocabulary/#dfn-object
|
||||
* @property {string} id -
|
||||
* @property {string} type - Ex: 'Object'
|
||||
* @property {APObject|APLink[]} attachment -
|
||||
* @property {APObject|APLink[]} attributedTo - Ex: ["canonical", "preview"]
|
||||
* @property {APObject|APLink[]} audience - Ex: ["canonical", "preview"]
|
||||
* @property {string} content - The content or textual representation of the Object encoded as a JSON string.
|
||||
* @property {Object<string, string>} contentMap - Language-tagged values for translated content.
|
||||
* @property {APObject|APLink} context - Identifies the context within which the object exists or an activity was performed.
|
||||
* @property {string} name - A simple, human-readable, plain-text name for the object.
|
||||
* @property {Object<string, string>} nameMap - Language-tagged values for translated name.
|
||||
* @property {string} endTime - Ex: "2015-01-01T06:00:00-08:00"
|
||||
* @property {APObject|APLink} generator - Identifies the entity (e.g. an application) that generated the object.
|
||||
* @property {APObject|APLink} icon -
|
||||
* @property {APObject} image -
|
||||
* @property {APObject|APLink} inReplyTo -
|
||||
* @property {APObject|APLink} location -
|
||||
* @property {APObject|APLink} preview -
|
||||
* @property {string} published - Ex: "2015-01-01T06:00:00-08:00"
|
||||
* @property {APCollection} replies -
|
||||
* @property {string} startTime - Ex: "2015-01-01T06:00:00-08:00"
|
||||
* @property {string} summary -
|
||||
* @property {(APObject|APLink)[]} tag -
|
||||
* @property {string} updated - Ex: "2015-01-01T06:00:00-08:00"
|
||||
* @property {string} url -
|
||||
* @property {APObject|APLink} to -
|
||||
* @property {APObject|APLink} bto -
|
||||
* @property {APObject|APLink} cc -
|
||||
* @property {APObject|APLink} bcc -
|
||||
* @property {string} mediaType - MIME Media Type. Ex: "text/html"
|
||||
* @property {string} duration - Ex: "PT2H"
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef APLink - https://www.w3.org/TR/activitystreams-vocabulary/#dfn-link
|
||||
* @property {'Link'} type - 'Link'
|
||||
* @property {string} href - The target resource pointed to by a Link. Ex: "http://example.org/abc"
|
||||
* @property {string[]} ref - Ex: ["canonical", "preview"]
|
||||
* @property {string} mediaType - MIME Media Type. Ex: "text/html"
|
||||
* @property {string} name - Ex: "An example name"
|
||||
* @property {string} hrefLang - A [BCP47] Language-Tag. Ex: "en"
|
||||
* @property {number} height - Ex: 100
|
||||
* @property {number} width - Ex: 100
|
||||
* @property {APObject|APLink} preview - Identifies an entity that provides a preview of this object.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {APObject} APCollection
|
||||
* @property {(APObject|APLink)[]} items -
|
||||
*/
|
||||
|
||||
export default {}
|
|
@ -0,0 +1,188 @@
|
|||
/**
|
||||
* @copyright Copyright (c) 2023 Louis Chmn <louis@chmn.me>
|
||||
*
|
||||
* @author Louis Chmn <louis@chmn.me>
|
||||
*
|
||||
* @license AGPL-3.0-or-later
|
||||
*
|
||||
* 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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef Field
|
||||
* @property {string} name - Ex: "Patreon"
|
||||
* @property {string} value - Ex: "<a href=\"https://www.patreon.com/mastodon\" rel=\"me nofollow noopener noreferrer\" target=\"_blank\"><span class=\"invisible\">https://www.</span><span class=\"\">patreon.com/mastodon</span><span class=\"invisible\"></span}"
|
||||
* @property {string} [verified_at] - Ex: "2019-12-08T03:48:33.901Z"
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef Card
|
||||
* @property {string} url - Ex: "https://www.theguardian.com/money/2019/dec/07/i-lost-my-193000-inheritance-with-one-wrong-digit-on-my-sort-code"
|
||||
* @property {string} title - Ex: "‘I lost my £193,000 inheritance – with one wrong digit on my sort code’"
|
||||
* @property {string} description - Ex: "When Peter Teich’s money went to another Barclays customer, the bank offered £25 as a token gesture"
|
||||
* @property {string} type - Ex: "link"
|
||||
* @property {string} author_name -
|
||||
* @property {string} author_url -
|
||||
* @property {string} provider_name -
|
||||
* @property {string} provider_url -
|
||||
* @property {string} html -
|
||||
* @property {number} width - Ex: 0
|
||||
* @property {number} height - Ex: 0
|
||||
* @property {number} [image] -
|
||||
* @property {string} embed_url -
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef Poll - https://docs.joinmastodon.org/entities/Poll/
|
||||
* @property {string} id - Ex: "34830"
|
||||
* @property {string} expires_at - Ex: "2019-12-05T04:05:08.302Z"
|
||||
* @property {boolean} expired - Ex: true
|
||||
* @property {boolean} multiple - Ex: false
|
||||
* @property {number} votes_count - Ex: 10
|
||||
* @property {number} [voters_count] - null
|
||||
* @property {boolean} voted - Ex: true
|
||||
* @property {number[]} own_votes - Ex: [1]
|
||||
* @property {PollOption[]} options - Ex: []
|
||||
* @property {CustomEmoji[]} emojis - []
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef PollOption
|
||||
* @property {string} title - Ex: "accept"
|
||||
* @property {number} votes_count - 6
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef StatusMention - https://docs.joinmastodon.org/entities/Status/#Mention
|
||||
* @property {string} id -
|
||||
* @property {string} username -
|
||||
* @property {string} url -
|
||||
* @property {string} acct -
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef StatusTag - https://docs.joinmastodon.org/entities/Status/#Tag
|
||||
* @property {string} name -
|
||||
* @property {string} url -
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef MediaAttachment - https://docs.joinmastodon.org/entities/MediaAttachment
|
||||
@property {string} id - Ex: "22345792"
|
||||
@property {string} type - Ex: "image"
|
||||
@property {string} url - Ex: "22345792"
|
||||
@property {string} preview_url - Ex: "https://files.mastodon.social/media_attachments/files/022/345/792/small/57859aede991da25.jpeg"
|
||||
@property {string} [remote_url] -
|
||||
@property {object} meta -
|
||||
@property {string} description - Ex: "test media description"
|
||||
@property {string} blurhash - Ex: "UFBWY:8_0Jxv4mx]t8t64.%M-:IUWGWAt6M}"
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef CustomEmoji
|
||||
* @property {string} shortcode - Ex: "blobaww"
|
||||
* @property {string} url - Ex: "https://files.mastodon.social/custom_emojis/images/000/011/739/original/blobaww.png"
|
||||
* @property {string} static_url - Ex: "static_url": "https://files.mastodon.social/custom_emojis/images/000/011/739/static/blobaww.png"
|
||||
* @property {boolean} visible_in_picker - Ex: "true"
|
||||
* @property {string} category - Ex: "Blobs"
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef Account - https://docs.joinmastodon.org/entities/Account
|
||||
@property {string} id - Ex: "22345792"
|
||||
* @property {string} username - Ex: "Gargron"
|
||||
* @property {string} acct - Ex: "Gargron@example.com or Gargron for local users"
|
||||
* @property {string} display_name - Ex: "Eugen"
|
||||
* @property {boolean} locked - Ex: false
|
||||
* @property {boolean} bot - Ex: false
|
||||
* @property {number} discoverable - Ex: true
|
||||
* @property {boolean} group - Ex: false
|
||||
* @property {string} created_at - Ex: "2016-03-16T14:34:26.392Z"
|
||||
* @property {string} note - Ex: "<p>Developer of Mastodon and administrator of mastodon.social. I post service announcements, development updates, and personal stuff.</p>"
|
||||
* @property {string} url - Ex: "https://mastodon.social/@Gargron"
|
||||
* @property {string} avatar - Ex: "https://files.mastodon.social/accounts/avatars/000/000/001/original/d96d39a0abb45b92.jpg"
|
||||
* @property {string} avatar_static - Ex: "https://files.mastodon.social/accounts/avatars/000/000/001/original/d96d39a0abb45b92.jpg"
|
||||
* @property {string} header - Ex: "https://files.mastodon.social/accounts/headers/000/000/001/original/c91b871f294ea63e.png"
|
||||
* @property {string} header_static - Ex: "https://files.mastodon.social/accounts/headers/000/000/001/original/c91b871f294ea63e.png"
|
||||
* @property {number} followers_count - Ex: 322930
|
||||
* @property {number} following_count - Ex: 459
|
||||
* @property {number} statuses_count - Ex: 61323
|
||||
* @property {string} last_status_at - Ex: "2019-12-10T08:14:44.811Z"
|
||||
* @property {CustomEmoji[]} emojis - Ex: []
|
||||
* @property {Field[]} fields - Ex: []
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef Status - https://docs.joinmastodon.org/entities/Status
|
||||
* @property {string} id - Ex: "103270115826048975"
|
||||
* @property {string} created_at - Ex: "2019-12-08T03:48:33.901Z"
|
||||
* @property {string} [in_reply_to_id] - Ex: Ex: "103270115826048975"
|
||||
* @property {number} [in_reply_to_account_id] - Ex: "1"
|
||||
* @property {boolean} sensitive - Ex: false
|
||||
* @property {string} spoiler_text -
|
||||
* @property {string} visibility - Ex: "public"
|
||||
* @property {string} language - Ex: "en"
|
||||
* @property {string} uri - Ex: "https://mastodon.social/users/Gargron/statuses/103270115826048975"
|
||||
* @property {string} url - Ex: "https://mastodon.social/@Gargron/103270115826048975"
|
||||
* @property {number} replies_count - Ex: 5
|
||||
* @property {number} reblogs_count - Ex: 6
|
||||
* @property {number} favourites_count - Ex: 11
|
||||
* @property {boolean} [favourited] - Ex: false
|
||||
* @property {boolean} [reblogged] - Ex: false
|
||||
* @property {boolean} [muted] - Ex: false
|
||||
* @property {boolean} [bookmarked] - Ex: false
|
||||
* @property {string} content - Ex: "<p>"I lost my inheritance with one wrong digit on my sort code"</p><p><a href=\"https://www.theguardian.com/money/2019/dec/07/i-lost-my-193000-inheritance-with-one-wrong-digit-on-my-sort-code\" rel=\"nofollow noopener noreferrer\" target=\"_blank\"><span class=\"invisible\">https://www.</span><span class=\"ellipsis\">theguardian.com/money/2019/dec</span><span class=\"invisible\">/07/i-lost-my-193000-inheritance-with-one-wrong-digit-on-my-sort-code</span}</p>"
|
||||
* @property {Status} [reblog] - Ex: null
|
||||
* @property {object} [application] -
|
||||
* @property {string} application.name - Ex: "Web"
|
||||
* @property {string} [application.website] - Ex: null
|
||||
* @property {Account} account -
|
||||
* @property {MediaAttachment[]} media_attachments - Ex: []
|
||||
* @property {StatusMention[]} mentions - Ex: []
|
||||
* @property {StatusTag[]} tags - Ex: []
|
||||
* @property {CustomEmoji[]} emojis - Ex: []
|
||||
* @property {Card} card -
|
||||
* @property {Poll} [poll] - Ex: null
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef Notification - https://docs.joinmastodon.org/entities/Notification
|
||||
* @property {string} id - Ex: "https://example.com/users/@tommy""
|
||||
* @property {"mention"|"status"|"reblog"|"follow"|"follow_request"|"favourite"|"poll"|"update"|"admin.sign_up"|"admin.report"} type - Ex: "2016-03-16T14:34:26.392Z"
|
||||
* @property {string} created_at - Ex: "2016-03-16T14:34:26.392Z"
|
||||
* @property {Account} account -
|
||||
* @property {Status} [status] -
|
||||
* @property {any} [report] -
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef Relationship - https://docs.joinmastodon.org/entities/Relationship
|
||||
* @property {string} id - The account ID. Ex: "https://example.com/users/@tommy""
|
||||
* @property {boolean} following - Are you following this user?
|
||||
* @property {boolean} showing_reblogs - Are you receiving this user’s boosts in your home timeline?
|
||||
* @property {boolean} notifying - Have you enabled notifications for this user?
|
||||
* @property {string[]} languages - Which languages are you following from this user?
|
||||
* @property {boolean} followed_by - Are you followed by this user?
|
||||
* @property {boolean} blocking - Are you blocking this user?
|
||||
* @property {boolean} blocked_by - Is this user blocking you?
|
||||
* @property {boolean} muting - Are you muting this user?
|
||||
* @property {boolean} muting_notifications - Are you muting notifications from this user?
|
||||
* @property {boolean} requested - Do you have a pending follow request for this user?
|
||||
* @property {boolean} domain_blocking - Are you blocking this user’s domain?
|
||||
* @property {boolean} endorsed - Are you featuring this user on your profile?
|
||||
* @property {string} note - This user’s profile bio
|
||||
*/
|
||||
|
||||
export default {}
|
|
@ -75,6 +75,7 @@ export default {
|
|||
},
|
||||
|
||||
computed: {
|
||||
/** @return {object[]} */
|
||||
items() {
|
||||
return this.notifications.map((n) => {
|
||||
return {
|
||||
|
@ -88,11 +89,13 @@ export default {
|
|||
}
|
||||
})
|
||||
},
|
||||
/** @return {number} */
|
||||
lastTimestamp() {
|
||||
return this.notifications.length
|
||||
? this.notifications[0].publishedTime
|
||||
: 0
|
||||
},
|
||||
/** @return {string} */
|
||||
emptyContentMessage() {
|
||||
if (this.state === 'error') {
|
||||
return t('social', 'Error getting Social notifications')
|
||||
|
@ -101,6 +104,7 @@ export default {
|
|||
}
|
||||
return ''
|
||||
},
|
||||
/** @return {string} */
|
||||
emptyContentIcon() {
|
||||
if (this.state === 'error') {
|
||||
return 'icon-close'
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
<div :class="{'icon-loading': !accountLoaded}" class="social__wrapper">
|
||||
<ProfileInfo 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" />
|
||||
<router-view v-if="accountLoaded && accountInfo && isLocal" name="details" />
|
||||
<NcEmptyContent v-if="accountLoaded && !accountInfo"
|
||||
:title="t('social', 'User not found')"
|
||||
:description="t('social', 'Sorry, we could not find the account of {userId}', { userId: uid })">
|
||||
|
@ -57,19 +57,22 @@ export default {
|
|||
data() {
|
||||
return {
|
||||
state: [],
|
||||
/** @type {string|null} */
|
||||
uid: null,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
/** @return {import('../types/Mastodon').Status[]} */
|
||||
timeline() {
|
||||
return this.$store.getters.getTimeline
|
||||
},
|
||||
/** @return {string} */
|
||||
emptyContentImage() {
|
||||
return generateFilePath('social', 'img', 'undraw/profile.svg')
|
||||
},
|
||||
},
|
||||
// Start fetching account information before mounting the component
|
||||
beforeMount() {
|
||||
async beforeMount() {
|
||||
this.uid = this.$route.params.account || this.serverData.account
|
||||
|
||||
// Are we authenticated?
|
||||
|
@ -82,9 +85,10 @@ export default {
|
|||
|
||||
// We need to update this.uid because we may have asked info for an account whose domain part was a host-meta,
|
||||
// and the account returned by the backend always uses a non host-meta'ed domain for its ID
|
||||
this.$store.dispatch(fetchMethod, this.profileAccount).then((response) => {
|
||||
this.uid = response.account
|
||||
})
|
||||
/** @type {[import('../types/Mastodon').Account]} */
|
||||
const response = await this.$store.dispatch(fetchMethod, this.profileAccount)
|
||||
this.uid = response.acct
|
||||
await this.$store.dispatch('fetchAccountRelationshipInfo', [this.accountInfo.id])
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -39,9 +39,11 @@ export default {
|
|||
serverData,
|
||||
],
|
||||
computed: {
|
||||
/** @return {string} */
|
||||
profileAccount() {
|
||||
return (this.$route.params.account.indexOf('@') === -1) ? this.$route.params.account + '@' + this.hostname : this.$route.params.account
|
||||
},
|
||||
/** @return {import('../types/Mastodon.js').Account[]} */
|
||||
users() {
|
||||
if (this.$route.name === 'profile.followers') {
|
||||
return this.$store.getters.getAccountFollowers(this.profileAccount)
|
||||
|
|
|
@ -34,7 +34,6 @@
|
|||
<script>
|
||||
import Composer from './../components/Composer/Composer.vue'
|
||||
import CurrentUserMixin from './../mixins/currentUserMixin.js'
|
||||
import follow from './../mixins/follow.js'
|
||||
import TimelineList from './../components/TimelineList.vue'
|
||||
|
||||
export default {
|
||||
|
@ -45,7 +44,6 @@ export default {
|
|||
},
|
||||
mixins: [
|
||||
CurrentUserMixin,
|
||||
follow,
|
||||
],
|
||||
data() {
|
||||
return {
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<div class="social__wrapper">
|
||||
<ProfileInfo v-if="accountLoaded && accountInfo" :uid="uid" />
|
||||
<Composer v-show="composerDisplayStatus" />
|
||||
<TimelineEntry class="main-post" :item="mainPost" />
|
||||
<TimelineEntry class="main-post" :item="mainPost" type="single-post" />
|
||||
<TimelineList v-if="timeline" :type="$route.params.type" />
|
||||
</div>
|
||||
</template>
|
||||
|
@ -72,14 +72,14 @@ export default {
|
|||
this.$store.dispatch(this.serverData.public ? 'fetchPublicAccountInfo' : 'fetchAccountInfo', this.account).then((response) => {
|
||||
// We need to update this.uid because we may have asked info for an account whose domain part was a host-meta,
|
||||
// and the account returned by the backend always uses a non host-meta'ed domain for its ID
|
||||
this.uid = response.account
|
||||
this.uid = response.username
|
||||
})
|
||||
|
||||
// Fetch single post timeline
|
||||
const params = {
|
||||
account: this.account,
|
||||
id: window.location.href,
|
||||
localId: window.location.href.split('/')[window.location.href.split('/').length - 1],
|
||||
localId: this.mainPost.id,
|
||||
type: 'single-post',
|
||||
}
|
||||
this.$store.dispatch('changeTimelineType', {
|
||||
|
|
|
@ -1,8 +1,15 @@
|
|||
// SPDX-FileCopyrigthText: 2022 Carl Schwan <carl@carlschwan.eu>
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
const path = require('path');
|
||||
const path = require('path')
|
||||
const webpackConfig = require('@nextcloud/webpack-vue-config')
|
||||
const CopyPlugin = require('copy-webpack-plugin')
|
||||
|
||||
webpackConfig.plugins.push(new CopyPlugin({
|
||||
patterns: [
|
||||
{ from: 'node_modules/twemoji/2/svg/', to: '../img/twemoji' },
|
||||
],
|
||||
}))
|
||||
|
||||
webpackConfig.entry = {
|
||||
social: path.join(__dirname, 'src', 'main.js'),
|
||||
|
|
Ładowanie…
Reference in New Issue