kopia lustrzana https://github.com/nextcloud/social
Use new Mastodon like API
Revert "Revert "Merge pull request #1581 from nextcloud/artonge/feat/use_new_api""
This reverts commit c4eef0b2d4
.
Signed-off-by: Louis Chemineau <louis@chmn.me>
Adapt views to new timeline api
Signed-off-by: Louis Chemineau <louis@chmn.me>
Add types
Signed-off-by: Louis Chemineau <louis@chmn.me>
Fix type errors
Signed-off-by: Louis Chemineau <louis@chmn.me>
Adapte front-end to mastodon data format
+ Add typing
+ Modernize code
Signed-off-by: Louis Chemineau <louis@chmn.me>
Use new API for media attachments
+ Split Composer.vue into tinier composent
+ Use blurhash value of attachments
Signed-off-by: Louis Chemineau <louis@chmn.me>
Fix media attachment rendering in post component
Signed-off-by: Louis Chemineau <louis@chmn.me>
Use square container to display statuses attachments
Signed-off-by: Louis Chemineau <louis@chmn.me>
Add typing to timeline.js
And fix type errors
Signed-off-by: Louis Chemineau <louis@chmn.me>
Forward format to getStreamSelectSql for direct timeline
Signed-off-by: Louis Chemineau <louis@chmn.me>
Fix liked timeline
Signed-off-by: Louis Chemineau <louis@chmn.me>
Use new API for local and federated timelines
Signed-off-by: Louis Chemineau <louis@chmn.me>
Fix profile and avatar for local users
Signed-off-by: Louis Chemineau <louis@chmn.me>
Update babel config
Signed-off-by: Louis Chemineau <louis@chmn.me>
Improve typing in account.js
Signed-off-by: Louis Chemineau <louis@chmn.me>
Handle new notification format
Signed-off-by: Louis Chemineau <louis@chmn.me>
Fix follow button
Signed-off-by: Louis Chemineau <louis@chmn.me>
Fix condition of delete button for statuses
Signed-off-by: Louis Chemineau <louis@chmn.me>
Add relationship fetching
Signed-off-by: Louis Chemineau <louis@chmn.me>
Improve attachments viewer
Signed-off-by: Louis Chemineau <louis@chmn.me>
Correctly use twemoji
Signed-off-by: Louis Chemineau <louis@chmn.me>
Clean up composer
Signed-off-by: Louis Chemineau <louis@chmn.me>
Insert emoji on the last line instead of creating a new one
Signed-off-by: Louis Chemineau <louis@chmn.me>
Overall improvements in composer
Signed-off-by: Louis Chemineau <louis@chmn.me>
Clean up PreviewGridItem
Signed-off-by: Louis Chemineau <louis@chmn.me>
Fix fetching relationships
Signed-off-by: Louis Chemineau <louis@chmn.me>
Fix followers and following list
Signed-off-by: Louis Chemineau <louis@chmn.me>
Fix direct link to followers and following lists
Signed-off-by: Louis Chemineau <louis@chmn.me>
Fix notifications endpoint
Signed-off-by: Louis Chemineau <louis@chmn.me>
Handle different types of notifications
Signed-off-by: Louis Chemineau <louis@chmn.me>
Add formatted date as title for statuses
Signed-off-by: Louis Chemineau <louis@chmn.me>
Fix entryContent computed property
Signed-off-by: Louis Chemineau <louis@chmn.me>
Handle reblog
Signed-off-by: Louis Chemineau <louis@chmn.me>
Fix favourite type
Signed-off-by: Louis Chemineau <louis@chmn.me>
Load context of status for single post
Signed-off-by: Louis Chemineau <louis@chmn.me>
Use new format to set the uid in single post
Signed-off-by: Louis Chemineau <louis@chmn.me>
Fix display name property
Signed-off-by: Louis Chemineau <louis@chmn.me>
Hack to handle context of single post
Signed-off-by: Louis Chemineau <louis@chmn.me>
Use item id to fetch context
Signed-off-by: Louis Chemineau <louis@chmn.me>
Remove unsused variable
Signed-off-by: Louis Chemineau <louis@chmn.me>
pull/1615/head
rodzic
991a49c7b9
commit
ab347790a8
15
.babelrc.js
15
.babelrc.js
|
@ -1,14 +1,3 @@
|
||||||
module.exports = {
|
const babelConfig = require('@nextcloud/babel-config')
|
||||||
plugins: ['@babel/plugin-syntax-dynamic-import'],
|
|
||||||
presets: [
|
|
||||||
[
|
|
||||||
'@babel/preset-env',
|
|
||||||
{
|
|
||||||
targets: {
|
|
||||||
browsers: ['last 2 versions', 'ie >= 11']
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
|
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 $limit = 20,
|
||||||
int $max_id = 0,
|
int $max_id = 0,
|
||||||
int $min_id = 0,
|
int $min_id = 0,
|
||||||
int $since = 0
|
int $since_id = 0
|
||||||
): DataResponse {
|
): DataResponse {
|
||||||
try {
|
try {
|
||||||
$this->initViewer(true);
|
$this->initViewer(true);
|
||||||
|
@ -568,7 +568,7 @@ class ApiController extends Controller {
|
||||||
->setLimit($limit)
|
->setLimit($limit)
|
||||||
->setMaxId($max_id)
|
->setMaxId($max_id)
|
||||||
->setMinId($min_id)
|
->setMinId($min_id)
|
||||||
->setSince($since);
|
->setSince($since_id);
|
||||||
|
|
||||||
$posts = $this->streamService->getTimeline($options);
|
$posts = $this->streamService->getTimeline($options);
|
||||||
|
|
||||||
|
|
|
@ -37,6 +37,7 @@ use Exception;
|
||||||
use OCA\Social\AppInfo\Application;
|
use OCA\Social\AppInfo\Application;
|
||||||
use OCA\Social\Exceptions\AccountDoesNotExistException;
|
use OCA\Social\Exceptions\AccountDoesNotExistException;
|
||||||
use OCA\Social\Exceptions\InvalidResourceException;
|
use OCA\Social\Exceptions\InvalidResourceException;
|
||||||
|
use OCA\Social\Model\ActivityPub\ACore;
|
||||||
use OCA\Social\Model\ActivityPub\Actor\Person;
|
use OCA\Social\Model\ActivityPub\Actor\Person;
|
||||||
use OCA\Social\Model\ActivityPub\Object\Note;
|
use OCA\Social\Model\ActivityPub\Object\Note;
|
||||||
use OCA\Social\Model\ActivityPub\Stream;
|
use OCA\Social\Model\ActivityPub\Stream;
|
||||||
|
@ -537,8 +538,9 @@ class LocalController extends Controller {
|
||||||
|
|
||||||
$actor = $this->cacheActorService->getFromLocalAccount($username);
|
$actor = $this->cacheActorService->getFromLocalAccount($username);
|
||||||
$actor->setCompleteDetails(true);
|
$actor->setCompleteDetails(true);
|
||||||
|
$actor->setExportFormat(ACore::FORMAT_LOCAL);
|
||||||
|
|
||||||
return $this->success(['account' => $actor]);
|
return new DataResponse($actor, Http::STATUS_OK);
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
return $this->fail($e);
|
return $this->fail($e);
|
||||||
}
|
}
|
||||||
|
@ -588,8 +590,9 @@ class LocalController extends Controller {
|
||||||
$this->initViewer();
|
$this->initViewer();
|
||||||
|
|
||||||
$actor = $this->cacheActorService->getFromAccount($account);
|
$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) {
|
} catch (Exception $e) {
|
||||||
return $this->fail($e);
|
return $this->fail($e);
|
||||||
}
|
}
|
||||||
|
|
|
@ -451,7 +451,7 @@ class StreamRequest extends StreamRequestBuilder {
|
||||||
* @return Stream[]
|
* @return Stream[]
|
||||||
*/
|
*/
|
||||||
private function getTimelineDirect(ProbeOptions $options): array {
|
private function getTimelineDirect(ProbeOptions $options): array {
|
||||||
$qb = $this->getStreamSelectSql();
|
$qb = $this->getStreamSelectSql($options->getFormat());
|
||||||
|
|
||||||
$qb->filterType(SocialAppNotification::TYPE);
|
$qb->filterType(SocialAppNotification::TYPE);
|
||||||
$qb->paginate($options);
|
$qb->paginate($options);
|
||||||
|
@ -476,7 +476,7 @@ class StreamRequest extends StreamRequestBuilder {
|
||||||
* @return Stream[]
|
* @return Stream[]
|
||||||
*/
|
*/
|
||||||
private function getTimelineAccount(ProbeOptions $options): array {
|
private function getTimelineAccount(ProbeOptions $options): array {
|
||||||
$qb = $this->getStreamSelectSql();
|
$qb = $this->getStreamSelectSql($options->getFormat());
|
||||||
|
|
||||||
$qb->filterType(SocialAppNotification::TYPE);
|
$qb->filterType(SocialAppNotification::TYPE);
|
||||||
$qb->paginate($options);
|
$qb->paginate($options);
|
||||||
|
|
|
@ -592,7 +592,7 @@ class Stream extends ACore implements IQueryRow, JsonSerializable {
|
||||||
public function exportAsNotification(): array {
|
public function exportAsNotification(): array {
|
||||||
switch ($this->getSubType()) {
|
switch ($this->getSubType()) {
|
||||||
case Like::TYPE:
|
case Like::TYPE:
|
||||||
$type = 'favourites';
|
$type = 'favourite';
|
||||||
break;
|
break;
|
||||||
case Announce::TYPE:
|
case Announce::TYPE:
|
||||||
$type = 'mention';
|
$type = 'mention';
|
||||||
|
|
|
@ -19,6 +19,8 @@
|
||||||
"@nextcloud/router": "^2.0.1",
|
"@nextcloud/router": "^2.0.1",
|
||||||
"@nextcloud/vue": "^7.4.0",
|
"@nextcloud/vue": "^7.4.0",
|
||||||
"@nextcloud/vue-richtext": "^2.0.4",
|
"@nextcloud/vue-richtext": "^2.0.4",
|
||||||
|
"blurhash": "^2.0.5",
|
||||||
|
"debounce": "^1.2.1",
|
||||||
"he": "^1.2.0",
|
"he": "^1.2.0",
|
||||||
"linkify-plugin-mention": "^4.1.0",
|
"linkify-plugin-mention": "^4.1.0",
|
||||||
"linkify-string": "^4.1.0",
|
"linkify-string": "^4.1.0",
|
||||||
|
@ -30,8 +32,8 @@
|
||||||
"v-tooltip": "^4.0.0-beta.0",
|
"v-tooltip": "^4.0.0-beta.0",
|
||||||
"vue": "^2.7.10",
|
"vue": "^2.7.10",
|
||||||
"vue-click-outside": "^1.0.7",
|
"vue-click-outside": "^1.0.7",
|
||||||
"vue-contenteditable-directive": "^1.2.0",
|
|
||||||
"vue-infinite-loading": "^2.4.4",
|
"vue-infinite-loading": "^2.4.4",
|
||||||
|
"vue-masonry-css": "^1.0.3",
|
||||||
"vue-material-design-icons": "^5.0.0",
|
"vue-material-design-icons": "^5.0.0",
|
||||||
"vue-router": "^3.6.5",
|
"vue-router": "^3.6.5",
|
||||||
"vue-tribute": "^1.0.7",
|
"vue-tribute": "^1.0.7",
|
||||||
|
@ -47,6 +49,7 @@
|
||||||
"@nextcloud/eslint-config": "^8.2.0",
|
"@nextcloud/eslint-config": "^8.2.0",
|
||||||
"@nextcloud/stylelint-config": "^2.3.0",
|
"@nextcloud/stylelint-config": "^2.3.0",
|
||||||
"@nextcloud/webpack-vue-config": "^5.4.0",
|
"@nextcloud/webpack-vue-config": "^5.4.0",
|
||||||
|
"copy-webpack-plugin": "^11.0.0",
|
||||||
"cypress": "^11.2.0",
|
"cypress": "^11.2.0",
|
||||||
"jest": "^29.3.1",
|
"jest": "^29.3.1",
|
||||||
"jest-serializer-vue": "^3.1.0",
|
"jest-serializer-vue": "^3.1.0",
|
||||||
|
@ -3513,7 +3516,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||||
"integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
|
"integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nodelib/fs.stat": "2.0.5",
|
"@nodelib/fs.stat": "2.0.5",
|
||||||
"run-parallel": "^1.1.9"
|
"run-parallel": "^1.1.9"
|
||||||
|
@ -3527,7 +3529,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
|
||||||
"integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
|
"integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
|
@ -3537,7 +3538,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
|
"resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
|
||||||
"integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
|
"integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nodelib/fs.scandir": "2.1.5",
|
"@nodelib/fs.scandir": "2.1.5",
|
||||||
"fastq": "^1.6.0"
|
"fastq": "^1.6.0"
|
||||||
|
@ -3796,8 +3796,7 @@
|
||||||
"version": "7.0.11",
|
"version": "7.0.11",
|
||||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz",
|
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz",
|
||||||
"integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==",
|
"integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==",
|
||||||
"dev": true,
|
"dev": true
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/@types/json5": {
|
"node_modules/@types/json5": {
|
||||||
"version": "0.0.29",
|
"version": "0.0.29",
|
||||||
|
@ -4798,7 +4797,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz",
|
||||||
"integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==",
|
"integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ajv": "^8.0.0"
|
"ajv": "^8.0.0"
|
||||||
},
|
},
|
||||||
|
@ -4816,7 +4814,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz",
|
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz",
|
||||||
"integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==",
|
"integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fast-deep-equal": "^3.1.1",
|
"fast-deep-equal": "^3.1.1",
|
||||||
"json-schema-traverse": "^1.0.0",
|
"json-schema-traverse": "^1.0.0",
|
||||||
|
@ -4832,8 +4829,7 @@
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
||||||
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
|
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
|
||||||
"dev": true,
|
"dev": true
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/ajv-keywords": {
|
"node_modules/ajv-keywords": {
|
||||||
"version": "3.5.2",
|
"version": "3.5.2",
|
||||||
|
@ -5585,6 +5581,11 @@
|
||||||
"integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==",
|
"integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==",
|
||||||
"dev": true
|
"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": {
|
"node_modules/bn.js": {
|
||||||
"version": "5.2.1",
|
"version": "5.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz",
|
||||||
|
@ -6779,6 +6780,126 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": 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": {
|
"node_modules/core-js": {
|
||||||
"version": "3.25.5",
|
"version": "3.25.5",
|
||||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.25.5.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
|
||||||
"integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==",
|
"integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"path-type": "^4.0.0"
|
"path-type": "^4.0.0"
|
||||||
},
|
},
|
||||||
|
@ -9189,15 +9309,13 @@
|
||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||||
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
||||||
"dev": true,
|
"dev": true
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/fast-glob": {
|
"node_modules/fast-glob": {
|
||||||
"version": "3.2.11",
|
"version": "3.2.11",
|
||||||
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz",
|
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz",
|
||||||
"integrity": "sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==",
|
"integrity": "sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nodelib/fs.stat": "^2.0.2",
|
"@nodelib/fs.stat": "^2.0.2",
|
||||||
"@nodelib/fs.walk": "^1.2.3",
|
"@nodelib/fs.walk": "^1.2.3",
|
||||||
|
@ -9258,7 +9376,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz",
|
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz",
|
||||||
"integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==",
|
"integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"reusify": "^1.0.4"
|
"reusify": "^1.0.4"
|
||||||
}
|
}
|
||||||
|
@ -10219,7 +10336,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz",
|
||||||
"integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==",
|
"integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 4"
|
"node": ">= 4"
|
||||||
}
|
}
|
||||||
|
@ -13565,7 +13681,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
|
||||||
"integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
|
"integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
|
@ -14425,7 +14540,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
|
||||||
"integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
|
"integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
|
@ -14899,8 +15013,7 @@
|
||||||
"type": "consulting",
|
"type": "consulting",
|
||||||
"url": "https://feross.org/support"
|
"url": "https://feross.org/support"
|
||||||
}
|
}
|
||||||
],
|
]
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/quick-lru": {
|
"node_modules/quick-lru": {
|
||||||
"version": "4.0.1",
|
"version": "4.0.1",
|
||||||
|
@ -15298,7 +15411,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
|
"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==",
|
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
|
@ -15405,7 +15517,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
|
||||||
"integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
|
"integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"iojs": ">=1.0.0",
|
"iojs": ">=1.0.0",
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
|
@ -15461,7 +15572,6 @@
|
||||||
"url": "https://feross.org/support"
|
"url": "https://feross.org/support"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"queue-microtask": "^1.2.2"
|
"queue-microtask": "^1.2.2"
|
||||||
}
|
}
|
||||||
|
@ -15633,7 +15743,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz",
|
||||||
"integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==",
|
"integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"randombytes": "^2.1.0"
|
"randombytes": "^2.1.0"
|
||||||
}
|
}
|
||||||
|
@ -17410,7 +17519,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz",
|
||||||
"integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==",
|
"integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"punycode": "^2.1.0"
|
"punycode": "^2.1.0"
|
||||||
}
|
}
|
||||||
|
@ -17582,11 +17690,6 @@
|
||||||
"tinycolor2": "^1.1.2"
|
"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": {
|
"node_modules/vue-eslint-parser": {
|
||||||
"version": "9.1.0",
|
"version": "9.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.1.0.tgz",
|
"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": {
|
"node_modules/vue-material-design-icons": {
|
||||||
"version": "5.1.2",
|
"version": "5.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/vue-material-design-icons/-/vue-material-design-icons-5.1.2.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||||
"integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
|
"integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"@nodelib/fs.stat": "2.0.5",
|
"@nodelib/fs.stat": "2.0.5",
|
||||||
"run-parallel": "^1.1.9"
|
"run-parallel": "^1.1.9"
|
||||||
|
@ -21223,15 +21330,13 @@
|
||||||
"version": "2.0.5",
|
"version": "2.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
|
||||||
"integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
|
"integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
|
||||||
"dev": true,
|
"dev": true
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"@nodelib/fs.walk": {
|
"@nodelib/fs.walk": {
|
||||||
"version": "1.2.8",
|
"version": "1.2.8",
|
||||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
|
"resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
|
||||||
"integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
|
"integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"@nodelib/fs.scandir": "2.1.5",
|
"@nodelib/fs.scandir": "2.1.5",
|
||||||
"fastq": "^1.6.0"
|
"fastq": "^1.6.0"
|
||||||
|
@ -21479,8 +21584,7 @@
|
||||||
"version": "7.0.11",
|
"version": "7.0.11",
|
||||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz",
|
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz",
|
||||||
"integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==",
|
"integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==",
|
||||||
"dev": true,
|
"dev": true
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"@types/json5": {
|
"@types/json5": {
|
||||||
"version": "0.0.29",
|
"version": "0.0.29",
|
||||||
|
@ -22279,7 +22383,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz",
|
||||||
"integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==",
|
"integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"ajv": "^8.0.0"
|
"ajv": "^8.0.0"
|
||||||
},
|
},
|
||||||
|
@ -22289,7 +22392,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz",
|
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz",
|
||||||
"integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==",
|
"integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"fast-deep-equal": "^3.1.1",
|
"fast-deep-equal": "^3.1.1",
|
||||||
"json-schema-traverse": "^1.0.0",
|
"json-schema-traverse": "^1.0.0",
|
||||||
|
@ -22301,8 +22403,7 @@
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
||||||
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
|
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
|
||||||
"dev": true,
|
"dev": true
|
||||||
"peer": true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -22878,6 +22979,11 @@
|
||||||
"integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==",
|
"integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==",
|
||||||
"dev": true
|
"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": {
|
"bn.js": {
|
||||||
"version": "5.2.1",
|
"version": "5.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz",
|
||||||
|
@ -23839,6 +23945,89 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": 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": {
|
"core-js": {
|
||||||
"version": "3.25.5",
|
"version": "3.25.5",
|
||||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.25.5.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
|
||||||
"integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==",
|
"integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"path-type": "^4.0.0"
|
"path-type": "^4.0.0"
|
||||||
}
|
}
|
||||||
|
@ -25677,15 +25865,13 @@
|
||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||||
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
||||||
"dev": true,
|
"dev": true
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"fast-glob": {
|
"fast-glob": {
|
||||||
"version": "3.2.11",
|
"version": "3.2.11",
|
||||||
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz",
|
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz",
|
||||||
"integrity": "sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==",
|
"integrity": "sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"@nodelib/fs.stat": "^2.0.2",
|
"@nodelib/fs.stat": "^2.0.2",
|
||||||
"@nodelib/fs.walk": "^1.2.3",
|
"@nodelib/fs.walk": "^1.2.3",
|
||||||
|
@ -25733,7 +25919,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz",
|
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz",
|
||||||
"integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==",
|
"integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"reusify": "^1.0.4"
|
"reusify": "^1.0.4"
|
||||||
}
|
}
|
||||||
|
@ -26452,8 +26637,7 @@
|
||||||
"version": "5.2.0",
|
"version": "5.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz",
|
||||||
"integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==",
|
"integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==",
|
||||||
"dev": true,
|
"dev": true
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"immutable": {
|
"immutable": {
|
||||||
"version": "4.1.0",
|
"version": "4.1.0",
|
||||||
|
@ -28954,8 +29138,7 @@
|
||||||
"version": "1.4.1",
|
"version": "1.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
|
||||||
"integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
|
"integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
|
||||||
"dev": true,
|
"dev": true
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"methods": {
|
"methods": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
|
@ -29617,8 +29800,7 @@
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
|
||||||
"integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
|
"integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
|
||||||
"dev": true,
|
"dev": true
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"pbkdf2": {
|
"pbkdf2": {
|
||||||
"version": "3.1.2",
|
"version": "3.1.2",
|
||||||
|
@ -29964,8 +30146,7 @@
|
||||||
"version": "1.2.3",
|
"version": "1.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
||||||
"integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
|
"integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
|
||||||
"dev": true,
|
"dev": true
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"quick-lru": {
|
"quick-lru": {
|
||||||
"version": "4.0.1",
|
"version": "4.0.1",
|
||||||
|
@ -30288,8 +30469,7 @@
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
|
"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==",
|
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
|
||||||
"dev": true,
|
"dev": true
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"requireindex": {
|
"requireindex": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
|
@ -30367,8 +30547,7 @@
|
||||||
"version": "1.0.4",
|
"version": "1.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
|
||||||
"integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
|
"integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
|
||||||
"dev": true,
|
"dev": true
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"rfdc": {
|
"rfdc": {
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
|
@ -30400,7 +30579,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
|
||||||
"integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
|
"integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"queue-microtask": "^1.2.2"
|
"queue-microtask": "^1.2.2"
|
||||||
}
|
}
|
||||||
|
@ -30526,7 +30704,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz",
|
||||||
"integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==",
|
"integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"randombytes": "^2.1.0"
|
"randombytes": "^2.1.0"
|
||||||
}
|
}
|
||||||
|
@ -31912,7 +32089,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz",
|
||||||
"integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==",
|
"integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"punycode": "^2.1.0"
|
"punycode": "^2.1.0"
|
||||||
}
|
}
|
||||||
|
@ -32067,11 +32243,6 @@
|
||||||
"tinycolor2": "^1.1.2"
|
"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": {
|
"vue-eslint-parser": {
|
||||||
"version": "9.1.0",
|
"version": "9.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.1.0.tgz",
|
"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-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": {
|
"vue-material-design-icons": {
|
||||||
"version": "5.1.2",
|
"version": "5.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/vue-material-design-icons/-/vue-material-design-icons-5.1.2.tgz",
|
"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/router": "^2.0.1",
|
||||||
"@nextcloud/vue": "^7.4.0",
|
"@nextcloud/vue": "^7.4.0",
|
||||||
"@nextcloud/vue-richtext": "^2.0.4",
|
"@nextcloud/vue-richtext": "^2.0.4",
|
||||||
|
"blurhash": "^2.0.5",
|
||||||
|
"debounce": "^1.2.1",
|
||||||
"he": "^1.2.0",
|
"he": "^1.2.0",
|
||||||
"linkify-plugin-mention": "^4.1.0",
|
"linkify-plugin-mention": "^4.1.0",
|
||||||
"linkify-string": "^4.1.0",
|
"linkify-string": "^4.1.0",
|
||||||
|
@ -53,8 +55,8 @@
|
||||||
"v-tooltip": "^4.0.0-beta.0",
|
"v-tooltip": "^4.0.0-beta.0",
|
||||||
"vue": "^2.7.10",
|
"vue": "^2.7.10",
|
||||||
"vue-click-outside": "^1.0.7",
|
"vue-click-outside": "^1.0.7",
|
||||||
"vue-contenteditable-directive": "^1.2.0",
|
|
||||||
"vue-infinite-loading": "^2.4.4",
|
"vue-infinite-loading": "^2.4.4",
|
||||||
|
"vue-masonry-css": "^1.0.3",
|
||||||
"vue-material-design-icons": "^5.0.0",
|
"vue-material-design-icons": "^5.0.0",
|
||||||
"vue-router": "^3.6.5",
|
"vue-router": "^3.6.5",
|
||||||
"vue-tribute": "^1.0.7",
|
"vue-tribute": "^1.0.7",
|
||||||
|
@ -80,6 +82,7 @@
|
||||||
"@nextcloud/eslint-config": "^8.2.0",
|
"@nextcloud/eslint-config": "^8.2.0",
|
||||||
"@nextcloud/stylelint-config": "^2.3.0",
|
"@nextcloud/stylelint-config": "^2.3.0",
|
||||||
"@nextcloud/webpack-vue-config": "^5.4.0",
|
"@nextcloud/webpack-vue-config": "^5.4.0",
|
||||||
|
"copy-webpack-plugin": "^11.0.0",
|
||||||
"cypress": "^11.2.0",
|
"cypress": "^11.2.0",
|
||||||
"jest": "^29.3.1",
|
"jest": "^29.3.1",
|
||||||
"jest-serializer-vue": "^3.1.0",
|
"jest-serializer-vue": "^3.1.0",
|
||||||
|
|
11
src/App.vue
11
src/App.vue
|
@ -106,9 +106,11 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
/** @return {import('vue').PropType<import('../types/Mastodon.js').Account>} */
|
||||||
timeline() {
|
timeline() {
|
||||||
return this.$store.getters.getTimeline
|
return this.$store.getters.getTimeline
|
||||||
},
|
},
|
||||||
|
/** @return {{items: {id: string, icon: object, title: string, to: { name: string } }, loading: boolean}} */
|
||||||
menu() {
|
menu() {
|
||||||
const defaultCategories = [
|
const defaultCategories = [
|
||||||
{
|
{
|
||||||
|
@ -152,7 +154,7 @@ export default {
|
||||||
title: t('social', 'Liked'),
|
title: t('social', 'Liked'),
|
||||||
to: {
|
to: {
|
||||||
name: 'timeline',
|
name: 'timeline',
|
||||||
params: { type: 'liked' },
|
params: { type: 'favourites' },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -263,3 +265,10 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
<style lang="css">
|
||||||
|
img.emoji {
|
||||||
|
margin: 3px;
|
||||||
|
width: 16px;
|
||||||
|
vertical-align: text-bottom;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -21,10 +21,10 @@
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<NcAvatar v-if="actor.local"
|
<NcAvatar v-if="isLocal"
|
||||||
:size="size"
|
:size="size"
|
||||||
:user="actor.preferredUsername"
|
:user="actor.username"
|
||||||
:display-name="actor.account"
|
:display-name="actor.acct"
|
||||||
:disable-tooltip="true"
|
:disable-tooltip="true"
|
||||||
:show-user-status="false" />
|
:show-user-status="false" />
|
||||||
<NcAvatar v-else
|
<NcAvatar v-else
|
||||||
|
@ -44,8 +44,15 @@ export default {
|
||||||
NcAvatar,
|
NcAvatar,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
actor: { type: Object, default: () => {} },
|
/** @type {import('vue').PropType<import('../types/Mastodon.js').Account>} */
|
||||||
size: { type: Number, default: 32 },
|
actor: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {},
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
type: Number,
|
||||||
|
default: 32,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
@ -53,9 +60,16 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
/** @return {string} */
|
||||||
avatarUrl() {
|
avatarUrl() {
|
||||||
return generateUrl('/apps/social/api/v1/global/actor/avatar?id=' + this.item.attributedTo)
|
return generateUrl('/apps/social/api/v1/global/actor/avatar?id=' + this.item.attributedTo)
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* @return {boolean}
|
||||||
|
*/
|
||||||
|
isLocal() {
|
||||||
|
return !this.actor.acct.includes('@')
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -26,6 +26,8 @@
|
||||||
<input id="file-upload"
|
<input id="file-upload"
|
||||||
ref="fileUploadInput"
|
ref="fileUploadInput"
|
||||||
type="file"
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
multiple="true"
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
class="hidden-visually"
|
class="hidden-visually"
|
||||||
|
@ -47,8 +49,8 @@
|
||||||
<div v-if="replyTo" class="reply-to">
|
<div v-if="replyTo" class="reply-to">
|
||||||
<p class="reply-info">
|
<p class="reply-info">
|
||||||
<span>{{ t('social', 'In reply to') }}</span>
|
<span>{{ t('social', 'In reply to') }}</span>
|
||||||
<ActorAvatar :actor="replyTo.actor_info" :size="16" />
|
<ActorAvatar :actor="replyTo.account" :size="16" />
|
||||||
<strong>{{ replyTo.actor_info.account }}</strong>
|
<strong>{{ replyTo.account.acct }}</strong>
|
||||||
<NcButton type="tertiary"
|
<NcButton type="tertiary"
|
||||||
class="close-button"
|
class="close-button"
|
||||||
:aria-label="t('social', 'Close reply')"
|
:aria-label="t('social', 'Close reply')"
|
||||||
|
@ -64,25 +66,24 @@
|
||||||
</div>
|
</div>
|
||||||
<form class="new-post-form" @submit.prevent="createPost">
|
<form class="new-post-form" @submit.prevent="createPost">
|
||||||
<VueTribute :options="tributeOptions">
|
<VueTribute :options="tributeOptions">
|
||||||
<!-- eslint-disable-next-line vue/valid-v-model -->
|
|
||||||
<div ref="composerInput"
|
<div ref="composerInput"
|
||||||
v-contenteditable:post.dangerousHTML="canType && !loading"
|
:disabled="loading"
|
||||||
class="message"
|
class="message"
|
||||||
placeholder="What would you like to share?"
|
placeholder="What would you like to share?"
|
||||||
:class="{'icon-loading': loading}"
|
:class="{'icon-loading': loading}"
|
||||||
@keyup.prevent.enter="keyup"
|
@keyup.prevent.enter="keyup"
|
||||||
|
@input="updateStatusContent"
|
||||||
@tribute-replaced="updatePostFromTribute" />
|
@tribute-replaced="updatePostFromTribute" />
|
||||||
</VueTribute>
|
</VueTribute>
|
||||||
|
|
||||||
<PreviewGrid :uploading="false"
|
<PreviewGrid :uploading="false"
|
||||||
:upload-progress="0.4"
|
:upload-progress="0.4"
|
||||||
:miniatures="previewUrls"
|
:miniatures="attachments"
|
||||||
@deleted="deletePreview" />
|
@deleted="deletePreview" />
|
||||||
|
|
||||||
<div class="options">
|
<div class="options">
|
||||||
<NcButton v-tooltip="t('social', 'Add attachment')"
|
<NcButton v-tooltip="t('social', 'Add attachment')"
|
||||||
type="tertiary"
|
type="tertiary"
|
||||||
:disabled="previewUrls.length >= 1"
|
|
||||||
:aria-label="t('social', 'Add attachment')"
|
:aria-label="t('social', 'Add attachment')"
|
||||||
@click.prevent="clickImportInput">
|
@click.prevent="clickImportInput">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
|
@ -94,7 +95,7 @@
|
||||||
<NcEmojiPicker ref="emojiPicker"
|
<NcEmojiPicker ref="emojiPicker"
|
||||||
:search="search"
|
:search="search"
|
||||||
:close-on-select="false"
|
:close-on-select="false"
|
||||||
:container="container"
|
container="#content-vue"
|
||||||
@select="insert">
|
@select="insert">
|
||||||
<NcButton v-tooltip="t('social', 'Add emoji')"
|
<NcButton v-tooltip="t('social', 'Add emoji')"
|
||||||
type="tertiary"
|
type="tertiary"
|
||||||
|
@ -107,18 +108,11 @@
|
||||||
</NcEmojiPicker>
|
</NcEmojiPicker>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-click-outside="hidePopoverMenu" class="popovermenu-parent">
|
<VisibilitySelect :type.sync="type" />
|
||||||
<NcButton v-tooltip="t('social', 'Visibility')"
|
|
||||||
type="tertiary"
|
|
||||||
:class="currentVisibilityIconClass"
|
|
||||||
@click.prevent="togglePopoverMenu" />
|
|
||||||
<div :class="{open: menuOpened}" class="popovermenu">
|
|
||||||
<NcPopoverMenu :menu="visibilityPopover" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="emptySpace" />
|
<div class="emptySpace" />
|
||||||
<NcButton :value="currentVisibilityPostLabel"
|
<SubmitStatusButton :type="type" :disabled="canPost || loading" @click="createPost" />
|
||||||
|
|
||||||
|
<!-- <NcButton :value="currentVisibilityPostLabel"
|
||||||
:disabled="!canPost"
|
:disabled="!canPost"
|
||||||
native-type="submit"
|
native-type="submit"
|
||||||
type="primary"
|
type="primary"
|
||||||
|
@ -127,7 +121,7 @@
|
||||||
<Send title="" :size="22" decorative />
|
<Send title="" :size="22" decorative />
|
||||||
</template>
|
</template>
|
||||||
{{ postTo }}
|
{{ postTo }}
|
||||||
</NcButton>
|
</NcButton> -->
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
@ -136,12 +130,11 @@
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
import EmoticonOutline from 'vue-material-design-icons/EmoticonOutline.vue'
|
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 Close from 'vue-material-design-icons/Close.vue'
|
||||||
import FileUpload from 'vue-material-design-icons/FileUpload.vue'
|
import FileUpload from 'vue-material-design-icons/FileUpload.vue'
|
||||||
|
import debounce from 'debounce'
|
||||||
import NcAvatar from '@nextcloud/vue/dist/Components/NcAvatar.js'
|
import NcAvatar from '@nextcloud/vue/dist/Components/NcAvatar.js'
|
||||||
import NcButton from '@nextcloud/vue/dist/Components/NcButton.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 NcEmojiPicker from '@nextcloud/vue/dist/Components/NcEmojiPicker.js'
|
||||||
import VueTribute from 'vue-tribute'
|
import VueTribute from 'vue-tribute'
|
||||||
import he from 'he'
|
import he from 'he'
|
||||||
|
@ -151,11 +144,18 @@ import axios from '@nextcloud/axios'
|
||||||
import ActorAvatar from '../ActorAvatar.vue'
|
import ActorAvatar from '../ActorAvatar.vue'
|
||||||
import { generateUrl } from '@nextcloud/router'
|
import { generateUrl } from '@nextcloud/router'
|
||||||
import PreviewGrid from './PreviewGrid.vue'
|
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 {
|
export default {
|
||||||
name: 'Composer',
|
name: 'Composer',
|
||||||
components: {
|
components: {
|
||||||
NcPopoverMenu,
|
|
||||||
NcAvatar,
|
NcAvatar,
|
||||||
NcEmojiPicker,
|
NcEmojiPicker,
|
||||||
NcButton,
|
NcButton,
|
||||||
|
@ -163,25 +163,24 @@ export default {
|
||||||
FileUpload,
|
FileUpload,
|
||||||
VueTribute,
|
VueTribute,
|
||||||
EmoticonOutline,
|
EmoticonOutline,
|
||||||
Send,
|
|
||||||
Close,
|
Close,
|
||||||
PreviewGrid,
|
PreviewGrid,
|
||||||
|
VisibilitySelect,
|
||||||
|
SubmitStatusButton,
|
||||||
},
|
},
|
||||||
directives: {
|
directives: {
|
||||||
FocusOnCreate,
|
FocusOnCreate,
|
||||||
},
|
},
|
||||||
mixins: [CurrentUserMixin],
|
mixins: [CurrentUserMixin],
|
||||||
props: {},
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
statusContent: '',
|
||||||
type: localStorage.getItem('social.lastPostType') || 'followers',
|
type: localStorage.getItem('social.lastPostType') || 'followers',
|
||||||
loading: false,
|
loading: false,
|
||||||
post: '',
|
/** @type {Object<string, LocalAttachment>} */
|
||||||
miniatures: [], // miniatures of images stored in postAttachments
|
attachments: {},
|
||||||
postAttachments: [], // The toot's attachments
|
|
||||||
previewUrls: [],
|
|
||||||
canType: true,
|
|
||||||
search: '',
|
search: '',
|
||||||
|
/** @type {import('../../types/Mastodon.js').Status} */
|
||||||
replyTo: null,
|
replyTo: null,
|
||||||
tributeOptions: {
|
tributeOptions: {
|
||||||
spaceSelectsMatch: true,
|
spaceSelectsMatch: true,
|
||||||
|
@ -201,25 +200,23 @@ export default {
|
||||||
return '<span class="mention" contenteditable="false">'
|
return '<span class="mention" contenteditable="false">'
|
||||||
+ '<a href="' + item.original.url + '" target="_blank"><img src="' + item.original.avatar + '" />@' + item.original.value + '</a></span>'
|
+ '<a href="' + item.original.url + '" target="_blank"><img src="' + item.original.avatar + '" />@' + item.original.value + '</a></span>'
|
||||||
},
|
},
|
||||||
values: (text, cb) => {
|
values: debounce(async (text, populate) => {
|
||||||
const users = []
|
|
||||||
|
|
||||||
if (text.length < 1) {
|
if (text.length < 1) {
|
||||||
cb(users)
|
populate([])
|
||||||
}
|
}
|
||||||
this.remoteSearchAccounts(text).then((result) => {
|
|
||||||
for (const i in result.data.result.accounts) {
|
const response = await this.remoteSearchAccounts(text)
|
||||||
const user = result.data.result.accounts[i]
|
|
||||||
users.push({
|
const users = response.data.result.accounts.map((user) => ({
|
||||||
key: user.preferredUsername,
|
key: user.preferredUsername,
|
||||||
value: user.account,
|
value: user.account,
|
||||||
url: user.url,
|
url: user.url,
|
||||||
avatar: user.local ? generateUrl(`/avatar/${user.preferredUsername}/32`) : generateUrl(`apps/social/api/v1/global/actor/avatar?id=${user.id}`),
|
avatar: user.local ? generateUrl(`/avatar/${user.preferredUsername}/32`) : generateUrl(`apps/social/api/v1/global/actor/avatar?id=${user.id}`),
|
||||||
})
|
}))
|
||||||
}
|
|
||||||
cb(users)
|
console.debug('[Composer] Found users for', text, response.data.result, users)
|
||||||
})
|
populate(users)
|
||||||
},
|
}, 200),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
trigger: '#',
|
trigger: '#',
|
||||||
|
@ -237,29 +234,20 @@ export default {
|
||||||
return '<span class="hashtag" contenteditable="false">'
|
return '<span class="hashtag" contenteditable="false">'
|
||||||
+ '<a href="' + generateUrl('/timeline/tags/' + tag) + '" target="_blank">#' + tag + '</a></span>'
|
+ '<a href="' + generateUrl('/timeline/tags/' + tag) + '" target="_blank">#' + tag + '</a></span>'
|
||||||
},
|
},
|
||||||
values: (text, cb) => {
|
values: debounce(async (text, populate) => {
|
||||||
const tags = []
|
|
||||||
|
|
||||||
if (text.length < 1) {
|
if (text.length < 1) {
|
||||||
cb(tags)
|
populate([])
|
||||||
}
|
}
|
||||||
this.remoteSearchHashtags(text).then((result) => {
|
|
||||||
if (result.data.result.exact) {
|
const response = await this.remoteSearchHashtags(text)
|
||||||
tags.push({
|
const tags = [
|
||||||
key: result.data.result.exact,
|
...(response.data.result.exact && !Array.isArray(response.data.result.exact) ? [{ key: response.data.result.exact, value: response.data.result.exact }] : []),
|
||||||
value: result.data.result.exact,
|
...response.data.result.tags.map(({ hashtag }) => ({ key: hashtag, value: hashtag })),
|
||||||
})
|
]
|
||||||
}
|
|
||||||
for (const i in result.data.result.tags) {
|
console.debug('[Composer] Found tags for', text, response.data.result, tags)
|
||||||
const tag = result.data.result.tags[i]
|
populate(tags)
|
||||||
tags.push({
|
}, 200),
|
||||||
key: tag.hashtag,
|
|
||||||
value: tag.hashtag,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
cb(tags)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
noMatchTemplate() {
|
noMatchTemplate() {
|
||||||
|
@ -272,123 +260,15 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
menuOpened: false,
|
|
||||||
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
postTo() {
|
/** @return {boolean} */
|
||||||
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)
|
|
||||||
},
|
|
||||||
canPost() {
|
canPost() {
|
||||||
if (this.previewUrls.length > 0) {
|
if (Object.keys(this.attachments).length > 0) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return this.post.length !== 0 && this.post !== '<br>'
|
return this.statusContent.length !== 0 && this.statusContent !== '<br>'
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
@ -398,95 +278,63 @@ export default {
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
updateStatusContent() {
|
||||||
|
this.statusContent = this.$refs.composerInput.innerHTML
|
||||||
|
},
|
||||||
clickImportInput() {
|
clickImportInput() {
|
||||||
this.$refs.fileUploadInput.click()
|
this.$refs.fileUploadInput.click()
|
||||||
},
|
},
|
||||||
|
/** @param {InputEvent} event */
|
||||||
handleFileChange(event) {
|
handleFileChange(event) {
|
||||||
event.target.files.forEach((file) => {
|
/** @type {HTMLInputElement} */
|
||||||
this.previewUrls.push({
|
const target = event.target
|
||||||
description: '',
|
Array.from(target.files).forEach(async (file) => {
|
||||||
url: URL.createObjectURL(file),
|
const url = URL.createObjectURL(file)
|
||||||
result: 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) {
|
insert(emoji) {
|
||||||
|
console.debug('[Composer] insert emoji', emoji)
|
||||||
if (typeof emoji === 'object') {
|
if (typeof emoji === 'object') {
|
||||||
const category = Object.keys(emoji)[0]
|
const category = Object.keys(emoji)[0]
|
||||||
const emojis = emoji[category]
|
const emojis = emoji[category]
|
||||||
const firstEmoji = Object.keys(emojis)[0]
|
const firstEmoji = Object.keys(emojis)[0]
|
||||||
emoji = emojis[firstEmoji]
|
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
|
if (lastChild === null) {
|
||||||
const to = []
|
this.$refs.composerInput.innerHTML = div.innerHTML
|
||||||
const mentionRegex = /<span class="mention"[^>]+><a[^>]+><img[^>]+>@([\w-_.]+@[\w-.]+)/g
|
} else {
|
||||||
let match = null
|
|
||||||
do {
|
// Content usually ends with </br> or </>
|
||||||
match = mentionRegex.exec(contentHtml)
|
// This makes sure that we put the emoji before those tags.
|
||||||
if (match) {
|
switch (lastChild.tagName) {
|
||||||
to.push(match[1])
|
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)
|
|
||||||
}
|
}
|
||||||
|
this.updateStatusContent()
|
||||||
// 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
|
|
||||||
},
|
},
|
||||||
keyup(event) {
|
keyup(event) {
|
||||||
if (event.shiftKey || event.ctrlKey) {
|
if (event.shiftKey || event.ctrlKey) {
|
||||||
|
@ -494,45 +342,44 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
updatePostFromTribute(event) {
|
updatePostFromTribute(event) {
|
||||||
// Trick to let vue-contenteditable know that tribute replaced a mention or hashtag
|
console.debug('[Composer] update from tribute', event)
|
||||||
this.$refs.composerInput.oninput(event)
|
this.updateStatusContent()
|
||||||
},
|
},
|
||||||
async createPost(event) {
|
async createPost(event) {
|
||||||
|
// Replace emoji <img> tag with actual emojis.
|
||||||
const postData = this.getPostData()
|
// They will be replaced again with twemoji during rendering
|
||||||
|
const element = this.$refs.composerInput.cloneNode(true)
|
||||||
// Trick to validate last mention when the user directly clicks on the "post" button without validating it.
|
Array.from(element.getElementsByClassName('emoji')).forEach((emoji) => {
|
||||||
const regex = /@([-\w]+)$/
|
const em = document.createTextNode(emoji.getAttribute('alt'))
|
||||||
const lastMention = postData.get('content').match(regex)
|
emoji.replaceWith(em)
|
||||||
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')
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
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() {
|
closeReply() {
|
||||||
this.replyTo = null
|
this.replyTo = null
|
||||||
|
@ -540,13 +387,13 @@ export default {
|
||||||
this.$store.commit('setComposerDisplayStatus', false)
|
this.$store.commit('setComposerDisplayStatus', false)
|
||||||
},
|
},
|
||||||
remoteSearchAccounts(text) {
|
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) {
|
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) {
|
deletePreview(key) {
|
||||||
this.previewUrls.splice(index, 1)
|
this.$delete(this.attachments, key)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,10 +19,10 @@ SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="preview-grid">
|
<div class="preview-grid">
|
||||||
<PreviewGridItem v-for="(item, index) in miniatures"
|
<PreviewGridItem v-for="(item, randomKey) in miniatures"
|
||||||
:key="index"
|
:key="randomKey"
|
||||||
:preview="item"
|
:preview="item"
|
||||||
:index="index"
|
:random-key="randomKey"
|
||||||
@delete="deletePreview" />
|
@delete="deletePreview" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -31,6 +31,7 @@ SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
<script>
|
<script>
|
||||||
import PreviewGridItem from './PreviewGridItem.vue'
|
import PreviewGridItem from './PreviewGridItem.vue'
|
||||||
import FileUpload from 'vue-material-design-icons/FileUpload.vue'
|
import FileUpload from 'vue-material-design-icons/FileUpload.vue'
|
||||||
|
import { translate } from '@nextcloud/l10n'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'PreviewGrid',
|
name: 'PreviewGrid',
|
||||||
|
@ -47,15 +48,18 @@ export default {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
/** @type {import('vue').PropType<Object<string, import('./Composer.vue').LocalAttachment>>} */
|
||||||
miniatures: {
|
miniatures: {
|
||||||
type: Array,
|
type: Object,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
deletePreview(index) {
|
deletePreview(randomKey) {
|
||||||
this.$emit('deleted', index)
|
this.$emit('deleted', randomKey)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
t: translate,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,87 +1,46 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="preview-item-wrapper">
|
<div class="preview-item-wrapper">
|
||||||
<div class="preview-item" :style="backgroundStyle">
|
<div class="preview-item">
|
||||||
|
<MediaAttachment :attachment="preview.data" />
|
||||||
|
|
||||||
<div class="preview-item__actions">
|
<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>
|
<template #icon>
|
||||||
<Close :size="16" fill-color="white" />
|
<Close :size="16" fill-color="white" />
|
||||||
</template>
|
</template>
|
||||||
<span>{{ t('social', 'Delete') }}</span>
|
<span>{{ t('social', 'Delete') }}</span>
|
||||||
</NcButton>
|
</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>
|
||||||
|
|
||||||
<!--
|
|
||||||
<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>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Close from 'vue-material-design-icons/Close.vue'
|
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 NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
|
||||||
|
import { translate } from '@nextcloud/l10n'
|
||||||
|
import MediaAttachment from '../MediaAttachment.vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'PreviewGridItem',
|
name: 'PreviewGridItem',
|
||||||
components: {
|
components: {
|
||||||
Close,
|
Close,
|
||||||
// Edit,
|
|
||||||
// NcModal,
|
|
||||||
NcButton,
|
NcButton,
|
||||||
|
MediaAttachment,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
|
/** @type {import('vue').PropType<import('./Composer.vue').LocalAttachment>} */
|
||||||
preview: {
|
preview: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
index: {
|
randomKey: {
|
||||||
type: Number,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
modal: false,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
backgroundStyle() {
|
|
||||||
return {
|
|
||||||
backgroundImage: `url("${this.preview.url}")`,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
methods: {
|
methods: {
|
||||||
showModal() {
|
t: translate,
|
||||||
this.modal = true
|
|
||||||
},
|
|
||||||
closeModal() {
|
|
||||||
this.modal = false
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -95,7 +54,7 @@ export default {
|
||||||
|
|
||||||
.preview-item {
|
.preview-item {
|
||||||
border-radius: var(--border-radius-large);
|
border-radius: var(--border-radius-large);
|
||||||
background-color: #000;
|
background: var(--color-background-darker);
|
||||||
background-position: 50%;
|
background-position: 50%;
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
|
@ -109,6 +68,9 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
&__actions {
|
&__actions {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
background: linear-gradient(180deg,rgba(0,0,0,.8),rgba(0,0,0,.35) 80%,transparent);
|
background: linear-gradient(180deg,rgba(0,0,0,.8),rgba(0,0,0,.35) 80%,transparent);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
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 {
|
export default {
|
||||||
name: 'Emoji',
|
name: 'Emoji',
|
||||||
props: {
|
props: {
|
||||||
emoji: { type: String, default: '' },
|
emoji: {
|
||||||
},
|
type: String,
|
||||||
data() {
|
default: '',
|
||||||
return {}
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -21,39 +21,47 @@
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="emptycontent">
|
<NcEmptyContent :title="item.title" :description="item.description">
|
||||||
<img v-if="item.image"
|
<template v-if="item.image" #icon>
|
||||||
:src="imageUrl"
|
<img class="empty-content__image"
|
||||||
class="icon-illustration"
|
:src="imageUrl"
|
||||||
alt="">
|
alt="">
|
||||||
<h2>{{ item.title }}</h2>
|
</template>
|
||||||
<p>{{ item.description }}</p>
|
</NcEmptyContent>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
|
import { linkTo } from '@nextcloud/router'
|
||||||
|
import NcEmptyContent from '@nextcloud/vue/dist/Components/NcEmptyContent.js'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'EmptyContent',
|
name: 'EmptyContent',
|
||||||
|
components: {
|
||||||
|
NcEmptyContent,
|
||||||
|
},
|
||||||
props: {
|
props: {
|
||||||
item: { type: Object, default: () => {} },
|
item: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
/** @return {string} */
|
||||||
imageUrl() {
|
imageUrl() {
|
||||||
return OC.linkTo('social', this.item.image)
|
return linkTo('social', this.item.image)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.emptycontent {
|
.empty-content__image {
|
||||||
margin-top: 5vh;
|
height: 256px;
|
||||||
}
|
width: 256px;
|
||||||
|
}
|
||||||
|
|
||||||
.emptycontent .icon-illustration {
|
:deep(.empty-content__icon) {
|
||||||
height: 256px;
|
opacity: 1;
|
||||||
width: 256px;
|
margin-bottom: 90px;
|
||||||
margin: 0;
|
}
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -22,8 +22,8 @@
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<!-- Show button only if user is authenticated and she is not the same as the account viewed -->
|
<!-- 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="!serverData.public && relationship !== undefined">
|
||||||
<div v-if="isCurrentUserFollowing"
|
<div v-if="relationship.following"
|
||||||
class="follow-button-container">
|
class="follow-button-container">
|
||||||
<NcButton :disabled="loading"
|
<NcButton :disabled="loading"
|
||||||
class="follow-button follow-button--following"
|
class="follow-button follow-button--following"
|
||||||
|
@ -87,16 +87,20 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
/** @return {boolean} */
|
||||||
isCurrentUserFollowing() {
|
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: {
|
methods: {
|
||||||
async follow() {
|
async follow() {
|
||||||
try {
|
try {
|
||||||
this.loading = true
|
this.loading = true
|
||||||
await this.$store.dispatch('followAccount', { currentAccount: this.cloudId, accountToFollow: this.account })
|
await this.$store.dispatch('followAccount', { currentAccount: this.cloudId, accountToFollow: this.profileAccount })
|
||||||
} catch {
|
|
||||||
} finally {
|
} finally {
|
||||||
this.loading = false
|
this.loading = false
|
||||||
}
|
}
|
||||||
|
@ -104,8 +108,7 @@ export default {
|
||||||
async unfollow() {
|
async unfollow() {
|
||||||
try {
|
try {
|
||||||
this.loading = true
|
this.loading = true
|
||||||
await this.$store.dispatch('unfollowAccount', { currentAccount: this.cloudId, accountToUnfollow: this.account })
|
await this.$store.dispatch('unfollowAccount', { currentAccount: this.cloudId, accountToUnfollow: this.profileAccount })
|
||||||
} catch {
|
|
||||||
} finally {
|
} finally {
|
||||||
this.loading = false
|
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 Vue from 'vue'
|
||||||
import Emoji from './Emoji.vue'
|
import Emoji from './Emoji.vue'
|
||||||
|
|
||||||
/**
|
|
||||||
* @typedef {object} MessageSource
|
|
||||||
* @property {Array} tag
|
|
||||||
* @property {string} content
|
|
||||||
*/
|
|
||||||
|
|
||||||
export default Vue.component('MessageContent', {
|
export default Vue.component('MessageContent', {
|
||||||
props: {
|
props: {
|
||||||
source: {
|
item: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
render(createElement) {
|
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>.
|
* 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
|
* 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 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 {Function} createElement
|
||||||
* @param {MessageSource} source
|
* @param {import('../types/Mastodon').Status} item
|
||||||
*/
|
*/
|
||||||
export function formatMessage(createElement, source) {
|
export function formatMessage(createElement, item) {
|
||||||
if (!source.tag) {
|
if (!item.tags) {
|
||||||
source.tag = []
|
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 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 element = dom.getElementById('rootwrapper')
|
||||||
const cleaned = cleanCopy(createElement, element, { mentions, hashtags })
|
const cleaned = cleanCopy(createElement, element, item)
|
||||||
return cleaned
|
return cleaned
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -50,7 +41,7 @@ export function formatMessage(createElement, source) {
|
||||||
*
|
*
|
||||||
* @param {Function} createElement
|
* @param {Function} createElement
|
||||||
* @param {HTMLElement} node
|
* @param {HTMLElement} node
|
||||||
* @param {object} context
|
* @param {import('../types/Mastodon').Status} context
|
||||||
*/
|
*/
|
||||||
function domToVue(createElement, node, context) {
|
function domToVue(createElement, node, context) {
|
||||||
switch (node.tagName) {
|
switch (node.tagName) {
|
||||||
|
@ -133,7 +124,7 @@ function transformText(createElement, text) {
|
||||||
*
|
*
|
||||||
* @param {Function} createElement
|
* @param {Function} createElement
|
||||||
* @param {HTMLElement} node
|
* @param {HTMLElement} node
|
||||||
* @param {object} context
|
* @param {import('../types/Mastodon').Status} context
|
||||||
*/
|
*/
|
||||||
function cleanCopy(createElement, node, context) {
|
function cleanCopy(createElement, node, context) {
|
||||||
const children = Array.from(node.childNodes).map(node => domToVue(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 {Function} createElement
|
||||||
* @param {HTMLLinkElement} node
|
* @param {HTMLLinkElement} node
|
||||||
* @param {object} context
|
* @param {import('../types/Mastodon').Status} context
|
||||||
* @param {Array} context.mentions
|
|
||||||
*/
|
*/
|
||||||
function cleanLink(createElement, node, context) {
|
function cleanLink(createElement, node, context) {
|
||||||
const type = getLinkType(node.className)
|
const type = getLinkType(node.className)
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="post-attachments">
|
<div class="post-attachments">
|
||||||
<div v-for="(item, index) in attachments"
|
<div class="attachments-container">
|
||||||
:key="index"
|
<div v-for="(item, index) in attachementsSlice"
|
||||||
class="post-attachment"
|
:key="index"
|
||||||
@click="showModal(index)">
|
class="attachment"
|
||||||
<img v-if="item.mimeType.startsWith('image/')" :src="imageUrl(item)">
|
@click="showModal(index)">
|
||||||
<div v-else>
|
<MediaAttachment :attachment="item" />
|
||||||
{{ item }}
|
</div>
|
||||||
|
<div v-if="attachments.length > 4" class="attachment more-attachments">
|
||||||
|
+
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<NcModal v-if="modal"
|
<NcModal v-if="modal"
|
||||||
|
@ -14,10 +16,10 @@
|
||||||
:has-next="current < (attachments.length - 1)"
|
:has-next="current < (attachments.length - 1)"
|
||||||
size="full"
|
size="full"
|
||||||
@close="closeModal"
|
@close="closeModal"
|
||||||
@previous="showPrevious"
|
@previous="current--"
|
||||||
@next="showNext">
|
@next="current++">
|
||||||
<div class="modal__content">
|
<div class="attachment__viewer">
|
||||||
<canvas ref="modalCanvas" />
|
<img :src="attachments[current].url" :alt="attachments[current].description">
|
||||||
</div>
|
</div>
|
||||||
</NcModal>
|
</NcModal>
|
||||||
</div>
|
</div>
|
||||||
|
@ -26,17 +28,19 @@
|
||||||
<script>
|
<script>
|
||||||
import serverData from '../mixins/serverData.js'
|
import serverData from '../mixins/serverData.js'
|
||||||
import NcModal from '@nextcloud/vue/dist/Components/NcModal.js'
|
import NcModal from '@nextcloud/vue/dist/Components/NcModal.js'
|
||||||
import { generateUrl } from '@nextcloud/router'
|
import MediaAttachment from './MediaAttachment.vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'PostAttachment',
|
name: 'PostAttachment',
|
||||||
components: {
|
components: {
|
||||||
NcModal,
|
NcModal,
|
||||||
|
MediaAttachment,
|
||||||
},
|
},
|
||||||
mixins: [
|
mixins: [
|
||||||
serverData,
|
serverData,
|
||||||
],
|
],
|
||||||
props: {
|
props: {
|
||||||
|
/** @type {import('vue').PropType<import('../types/Mastodon.js').MediaAttachment[]>} */
|
||||||
attachments: {
|
attachments: {
|
||||||
type: Array,
|
type: Array,
|
||||||
default: Array,
|
default: Array,
|
||||||
|
@ -45,90 +49,72 @@ export default {
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
modal: false,
|
modal: false,
|
||||||
current: '',
|
current: 0,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
computed: {
|
||||||
/**
|
/** @return {import('../types/Mastodon.js').MediaAttachment[]} */
|
||||||
* @function imageUrl
|
attachementsSlice() {
|
||||||
* @description Returns the URL where to get a resized version of the attachement
|
if (this.attachments.length <= 4) {
|
||||||
* @param {object} item - The attachment
|
return this.attachments
|
||||||
* @return {string} The URL
|
|
||||||
*/
|
|
||||||
imageUrl(item) {
|
|
||||||
if (this.serverData.public) {
|
|
||||||
return generateUrl('/apps/social/document/public/resized?id=' + item.id)
|
|
||||||
} else {
|
} else {
|
||||||
return generateUrl('/apps/social/document/get/resized?id=' + item.id)
|
return this.attachments.slice(0, 3)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
/**
|
},
|
||||||
* @function displayImage
|
methods: {
|
||||||
* @description Displays the currently selected attachment's image
|
showModal(index) {
|
||||||
*/
|
this.current = index
|
||||||
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()
|
|
||||||
this.modal = true
|
this.modal = true
|
||||||
},
|
},
|
||||||
closeModal() {
|
closeModal() {
|
||||||
this.modal = false
|
this.modal = false
|
||||||
},
|
},
|
||||||
showPrevious() {
|
|
||||||
this.current--
|
|
||||||
this.displayImage()
|
|
||||||
},
|
|
||||||
showNext() {
|
|
||||||
this.current++
|
|
||||||
this.displayImage()
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.post-attachments {
|
.post-attachments {
|
||||||
margin-top: 12px;
|
.attachments-container {
|
||||||
width: 100%;
|
display: flex;
|
||||||
display: flex;
|
flex-wrap: wrap;
|
||||||
gap: 12px;
|
gap: 2px;
|
||||||
overflow-x: scroll;
|
margin-top: 12px;
|
||||||
|
width: 100%;
|
||||||
.post-attachment {
|
|
||||||
height: 100px;
|
|
||||||
object-fit: cover;
|
|
||||||
border-radius: var(--border-radius-large);
|
border-radius: var(--border-radius-large);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
flex-shrink: 0;
|
height: 40vh;
|
||||||
|
|
||||||
> * {
|
.attachment {
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-shrink: 1;
|
||||||
|
flex-basis: calc(50% - 2px);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
img {
|
.more-attachments {
|
||||||
width: 100%;
|
display: flex;
|
||||||
height: 100%;
|
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>
|
</style>
|
||||||
|
|
|
@ -22,7 +22,7 @@
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div v-if="profileAccount && accountInfo" class="user-profile">
|
<div v-if="profileAccount && accountInfo" class="user-profile">
|
||||||
<NcAvatar v-if="accountInfo.local"
|
<NcAvatar v-if="isLocal"
|
||||||
:user="localUid"
|
:user="localUid"
|
||||||
:disable-tooltip="true"
|
:disable-tooltip="true"
|
||||||
:size="128" />
|
:size="128" />
|
||||||
|
@ -32,32 +32,32 @@
|
||||||
:size="128" />
|
:size="128" />
|
||||||
<h2>{{ displayName }}</h2>
|
<h2>{{ displayName }}</h2>
|
||||||
<!-- TODO: we have no details, timeline and follower list for non-local accounts for now -->
|
<!-- 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>
|
<li>
|
||||||
<router-link :to="{ name: 'profile', params: { account: uid } }" class="icon-category-monitoring">
|
<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>
|
</router-link>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<router-link :to="{ name: 'profile.following', params: { account: uid } }" class="icon-category-social">
|
<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>
|
</router-link>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<router-link :to="{ name: 'profile.followers', params: { account: uid } }" class="icon-category-social">
|
<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>
|
</router-link>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p class="user-profile__info">
|
<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>
|
||||||
|
|
||||||
<p v-if="accountInfo.website" class="user-profile__info">
|
<p v-if="website" class="user-profile__info">
|
||||||
{{ t('social', 'Website') }}: <a :href="accountInfo.website.value">{{ accountInfo.website.value }}</a>
|
{{ t('social', 'Website') }}: <a :href="website.value">{{ website.value }}</a>
|
||||||
</p>
|
</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"
|
<NcButton v-if="serverData.public"
|
||||||
class="user-profile__info primary"
|
class="user-profile__info primary"
|
||||||
@click="followRemote">
|
@click="followRemote">
|
||||||
|
@ -72,9 +72,9 @@ import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
|
||||||
import accountMixins from '../mixins/accountMixins.js'
|
import accountMixins from '../mixins/accountMixins.js'
|
||||||
import serverData from '../mixins/serverData.js'
|
import serverData from '../mixins/serverData.js'
|
||||||
import currentUser from '../mixins/currentUserMixin.js'
|
import currentUser from '../mixins/currentUserMixin.js'
|
||||||
import follow from '../mixins/follow.js'
|
|
||||||
import FollowButton from './FollowButton.vue'
|
import FollowButton from './FollowButton.vue'
|
||||||
import { generateUrl } from '@nextcloud/router'
|
import { generateUrl } from '@nextcloud/router'
|
||||||
|
import { translate } from '@nextcloud/l10n'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'ProfileInfo',
|
name: 'ProfileInfo',
|
||||||
|
@ -87,7 +87,6 @@ export default {
|
||||||
accountMixins,
|
accountMixins,
|
||||||
currentUser,
|
currentUser,
|
||||||
serverData,
|
serverData,
|
||||||
follow,
|
|
||||||
],
|
],
|
||||||
props: {
|
props: {
|
||||||
uid: {
|
uid: {
|
||||||
|
@ -101,31 +100,30 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
/** @return {string} */
|
||||||
localUid() {
|
localUid() {
|
||||||
// Returns only the local part of a username
|
// Returns only the local part of a username
|
||||||
return (this.uid.indexOf('@') === -1) ? this.uid : this.uid.slice(0, this.uid.indexOf('@'))
|
return (this.uid.indexOf('@') === -1) ? this.uid : this.uid.slice(0, this.uid.indexOf('@'))
|
||||||
},
|
},
|
||||||
|
/** @return {string} */
|
||||||
displayName() {
|
displayName() {
|
||||||
if (typeof this.accountInfo.name !== 'undefined' && this.accountInfo.name !== '') {
|
return this.accountInfo.display_name ?? this.accountInfo.username ?? this.profileAccount
|
||||||
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 {string} */
|
||||||
avatarUrl() {
|
avatarUrl() {
|
||||||
return generateUrl('/apps/social/api/v1/global/actor/avatar?id=' + this.accountInfo.id)
|
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: {
|
methods: {
|
||||||
followRemote() {
|
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')
|
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 axios from '@nextcloud/axios'
|
||||||
import Trend from 'vuetrend'
|
import Trend from 'vuetrend'
|
||||||
import { generateUrl } from '@nextcloud/router'
|
import { generateUrl } from '@nextcloud/router'
|
||||||
|
import { translate } from '@nextcloud/l10n'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Search',
|
name: 'Search',
|
||||||
|
@ -80,6 +81,7 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
/** @return {import('../types/Mastodon.js').Account[]} */
|
||||||
allResults() {
|
allResults() {
|
||||||
if (this.results.accounts) {
|
if (this.results.accounts) {
|
||||||
if (this.results.accounts.exact) {
|
if (this.results.accounts.exact) {
|
||||||
|
@ -138,6 +140,8 @@ export default {
|
||||||
remoteSearch(term) {
|
remoteSearch(term) {
|
||||||
return axios.get(generateUrl('apps/social/api/v1/global/account/info?account=' + term))
|
return axios.get(generateUrl('apps/social/api/v1/global/account/info?account=' + term))
|
||||||
},
|
},
|
||||||
|
|
||||||
|
t: translate,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,14 +1,15 @@
|
||||||
<template>
|
<template>
|
||||||
<div v-if="item.actor_info" class="post-avatar">
|
<div v-if="item.account" class="post-avatar">
|
||||||
<NcAvatar v-if="item.local"
|
<NcAvatar v-if="isLocal"
|
||||||
class="messages__avatar__icon"
|
class="messages__avatar__icon"
|
||||||
:show-user-status="false"
|
:show-user-status="false"
|
||||||
menu-position="left"
|
menu-position="left"
|
||||||
:user="userTest"
|
:user="userTest"
|
||||||
:display-name="item.actor_info.account"
|
:display-name="item.account.acct"
|
||||||
|
:url="item.account.avatar"
|
||||||
:disable-tooltip="true" />
|
:disable-tooltip="true" />
|
||||||
<NcAvatar v-else
|
<NcAvatar v-else
|
||||||
:url="avatarUrl"
|
:url="item.account.avatar"
|
||||||
:disable-tooltip="true" />
|
:disable-tooltip="true" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -22,17 +23,22 @@ export default {
|
||||||
NcAvatar,
|
NcAvatar,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
|
/** @type {import('vue').PropType<import('../types/Mastodon.js').Status>} */
|
||||||
item: {
|
item: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => {},
|
default: () => {},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
/**
|
||||||
|
* @return {string}
|
||||||
|
*/
|
||||||
userTest() {
|
userTest() {
|
||||||
return this.item.actor_info.preferredUsername
|
return this.item.account.display_name
|
||||||
},
|
},
|
||||||
avatarUrl() {
|
/** @return {boolean} */
|
||||||
return OC.generateUrl('/apps/social/api/v1/global/actor/avatar?id=' + this.item.attributedTo)
|
isLocal() {
|
||||||
|
return !this.item.account.acct.includes('@')
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,36 +1,39 @@
|
||||||
<template>
|
<template>
|
||||||
<div :class="['timeline-entry', hasHeader ? 'with-header' : '']">
|
<div :class="['timeline-entry', hasHeader ? 'with-header' : '']">
|
||||||
<div v-if="item.type === 'SocialAppNotification'" class="notification">
|
<div v-if="isNotification" class="notification">
|
||||||
<Bell :size="22" />
|
<Bell :size="22" />
|
||||||
<span class="notification-action">
|
<span class="notification-action">
|
||||||
{{ actionSummary }}
|
{{ actionSummary }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<template v-else-if="item.type === 'Announce'">
|
<template v-else-if="isBoost">
|
||||||
<div class="container-icon-boost boost">
|
<div class="container-icon-boost boost">
|
||||||
<span class="icon-boost" />
|
<span class="icon-boost" />
|
||||||
</div>
|
</div>
|
||||||
<div class="boost">
|
<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 }}">
|
<router-link v-if="!isProfilePage && item.account"
|
||||||
<span v-tooltip.bottom="item.actor_info.account" class="post-author">
|
:to="{ name: 'profile', params: { account: item.account.username } }">
|
||||||
{{ userDisplayName(item.actor_info) }}
|
<span v-tooltip.bottom="item.account.acct" class="post-author">
|
||||||
|
{{ item.account.display_name }}
|
||||||
</span>
|
</span>
|
||||||
</router-link>
|
</router-link>
|
||||||
<a v-else :href="item.attributedTo">
|
<a v-else :href="item.account.id">
|
||||||
<span class="post-author-id">
|
<span class="post-author-id">
|
||||||
{{ item.attributedTo }}
|
{{ item.account.id }}
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
{{ boosted }}
|
{{ t('social', 'boosted') }}
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</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>
|
<template v-else>
|
||||||
<div class="wrapper">
|
<div class="wrapper">
|
||||||
<TimelineAvatar class="entry__avatar" :item="entryContent" />
|
<TimelineAvatar class="entry__avatar" :item="entryContent" />
|
||||||
<TimelinePost class="entry__content"
|
<TimelinePost class="entry__content"
|
||||||
:item="entryContent"
|
:item="entryContent"
|
||||||
:parent-announce="isBoost" />
|
:type="type" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
@ -41,6 +44,7 @@ import TimelinePost from './TimelinePost.vue'
|
||||||
import TimelineAvatar from './TimelineAvatar.vue'
|
import TimelineAvatar from './TimelineAvatar.vue'
|
||||||
import UserEntry from './UserEntry.vue'
|
import UserEntry from './UserEntry.vue'
|
||||||
import Bell from 'vue-material-design-icons/Bell.vue'
|
import Bell from 'vue-material-design-icons/Bell.vue'
|
||||||
|
import { translate } from '@nextcloud/l10n'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'TimelineEntry',
|
name: 'TimelineEntry',
|
||||||
|
@ -51,77 +55,91 @@ export default {
|
||||||
Bell,
|
Bell,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
|
/** @type {import('vue').PropType<import('../types/Mastodon.js').Status|import('../types/Mastodon.js').Notification>} */
|
||||||
item: {
|
item: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => {},
|
default: () => {},
|
||||||
},
|
},
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
isProfilePage: {
|
isProfilePage: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
computed: {
|
||||||
|
/**
|
||||||
|
* @return {import('../types/Mastodon.js').Status}
|
||||||
|
*/
|
||||||
entryContent() {
|
entryContent() {
|
||||||
if (this.item.type === 'Announce') {
|
if (this.isNotification) {
|
||||||
return this.item.cache[this.item.object].object
|
return this.notification.status
|
||||||
} else if (this.item.type === 'SocialAppNotification') {
|
|
||||||
return this.item.details.post
|
|
||||||
} else {
|
} else {
|
||||||
return this.item
|
return this.item
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
/** @return {boolean} */
|
||||||
|
isNotification() {
|
||||||
|
return this.item.type !== undefined
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* @return {boolean}
|
||||||
|
*/
|
||||||
isBoost() {
|
isBoost() {
|
||||||
if (this.item.type === 'Announce') {
|
return this.reblog !== null
|
||||||
return this.item
|
|
||||||
}
|
|
||||||
return {}
|
|
||||||
},
|
},
|
||||||
|
/** @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() {
|
hasHeader() {
|
||||||
return this.item.type === 'Announce' || this.item.type === 'SocialAppNotification'
|
return this.isBoost || this.isNotification
|
||||||
},
|
|
||||||
boosted() {
|
|
||||||
return t('social', 'boosted')
|
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* @return {string}
|
||||||
|
*/
|
||||||
actionSummary() {
|
actionSummary() {
|
||||||
|
switch (this.notification.type) {
|
||||||
let summary = this.item.summary
|
case 'mention':
|
||||||
for (const key in this.item.details) {
|
return t('social', '{account} mentioned you', { account: this.notification.account.acct })
|
||||||
|
case 'status':
|
||||||
const keyword = '{' + key + '}'
|
return t('social', '{account} has posted a status', { account: this.notification.account.acct })
|
||||||
if (typeof this.item.details[key] !== 'string' && this.item.details[key].length > 1) {
|
case 'reblog':
|
||||||
|
return t('social', '{account} boosted your post', { account: this.notification.account.acct })
|
||||||
let concatination = ''
|
case 'follow':
|
||||||
for (const stringKey in this.item.details[key]) {
|
return t('social', '{account} started following you', { account: this.notification.account.acct })
|
||||||
|
case 'follow_request':
|
||||||
if (this.item.details[key].length > 3 && stringKey === '3') {
|
return t('social', '{account} requested to follow you', { account: this.notification.account.acct })
|
||||||
// ellipses the actors' list to 3 actors when it's big
|
case 'favourite':
|
||||||
concatination = concatination.substring(0, concatination.length - 2)
|
return t('social', '{account} like you post', { account: this.notification.account.acct })
|
||||||
concatination += ' and ' + (this.item.details[key].length - 3).toString() + ' other(s), '
|
case 'poll':
|
||||||
break
|
return t('social', '{account} as ended the poll', { account: this.notification.account.acct })
|
||||||
} else {
|
case 'update':
|
||||||
concatination += this.item.details[key][stringKey] + ', '
|
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':
|
||||||
concatination = concatination.substring(0, concatination.length - 2)
|
return t('social', '{account} filled a report', { account: this.notification.account.acct })
|
||||||
summary = summary.replace(keyword, concatination)
|
default:
|
||||||
|
return ''
|
||||||
} else {
|
|
||||||
summary = summary.replace(keyword, this.item.details[key])
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return summary
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
userDisplayName(actorInfo) {
|
t: translate,
|
||||||
return actorInfo.name !== '' ? actorInfo.name : actorInfo.preferredUsername
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -23,7 +23,10 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="social__timeline">
|
<div class="social__timeline">
|
||||||
<transition-group name="list" tag="div">
|
<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>
|
</transition-group>
|
||||||
<InfiniteLoading ref="infiniteLoading" @infinite="infiniteHandler">
|
<InfiniteLoading ref="infiniteLoading" @infinite="infiniteHandler">
|
||||||
<div slot="spinner">
|
<div slot="spinner">
|
||||||
|
@ -41,10 +44,13 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import InfiniteLoading from 'vue-infinite-loading'
|
import InfiniteLoading from 'vue-infinite-loading'
|
||||||
|
|
||||||
|
import { showError } from '@nextcloud/dialogs'
|
||||||
|
|
||||||
import TimelineEntry from './TimelineEntry.vue'
|
import TimelineEntry from './TimelineEntry.vue'
|
||||||
import CurrentUserMixin from './../mixins/currentUserMixin.js'
|
import CurrentUserMixin from './../mixins/currentUserMixin.js'
|
||||||
import EmptyContent from './EmptyContent.vue'
|
import EmptyContent from './EmptyContent.vue'
|
||||||
import Logger from '../logger.js'
|
import logger from '../services/logger.js'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'TimelineList',
|
name: 'TimelineList',
|
||||||
|
@ -55,7 +61,10 @@ export default {
|
||||||
},
|
},
|
||||||
mixins: [CurrentUserMixin],
|
mixins: [CurrentUserMixin],
|
||||||
props: {
|
props: {
|
||||||
type: { type: String, default: () => 'home' },
|
type: {
|
||||||
|
type: String,
|
||||||
|
default: () => 'home',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
@ -87,7 +96,7 @@ export default {
|
||||||
title: t('social', 'No global posts found'),
|
title: t('social', 'No global posts found'),
|
||||||
description: t('social', 'Posts from federated instances will show up here'),
|
description: t('social', 'Posts from federated instances will show up here'),
|
||||||
},
|
},
|
||||||
liked: {
|
favourites: {
|
||||||
image: 'img/undraw/likes.svg',
|
image: 'img/undraw/likes.svg',
|
||||||
title: t('social', 'No liked posts found'),
|
title: t('social', 'No liked posts found'),
|
||||||
},
|
},
|
||||||
|
@ -121,9 +130,13 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback
|
// 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 this.emptyContent.default
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {import('../store/timeline.js').APObject[]}
|
||||||
|
*/
|
||||||
timeline() {
|
timeline() {
|
||||||
return this.$store.getters.getTimeline
|
return this.$store.getters.getTimeline
|
||||||
},
|
},
|
||||||
|
@ -132,22 +145,19 @@ export default {
|
||||||
|
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
infiniteHandler($state) {
|
async infiniteHandler($state) {
|
||||||
this.$store.dispatch('fetchTimeline', {
|
try {
|
||||||
account: this.currentUser.uid,
|
const response = await this.$store.dispatch('fetchTimeline', {
|
||||||
}).then((response) => {
|
account: this.currentUser.uid,
|
||||||
if (response.status === -1) {
|
max_id: this.timeline.length > 0 ? Number.parseInt(this.timeline[this.timeline.length - 1].id) : undefined,
|
||||||
OC.Notification.showTemporary('Failed to load more timeline entries')
|
})
|
||||||
console.error('Failed to load more timeline entries', response)
|
|
||||||
$state.complete()
|
response.length > 0 ? $state.loaded() : $state.complete()
|
||||||
return
|
} catch (error) {
|
||||||
}
|
showError('Failed to load more timeline entries')
|
||||||
response.result.length > 0 ? $state.loaded() : $state.complete()
|
logger.error('Failed to load more timeline entries', { error })
|
||||||
}).catch((error) => {
|
|
||||||
OC.Notification.showTemporary('Failed to load more timeline entries')
|
|
||||||
console.error('Failed to load more timeline entries', error)
|
|
||||||
$state.complete()
|
$state.complete()
|
||||||
})
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,35 +2,40 @@
|
||||||
<div class="post-content">
|
<div class="post-content">
|
||||||
<div class="post-header">
|
<div class="post-header">
|
||||||
<div class="post-author-wrapper">
|
<div class="post-author-wrapper">
|
||||||
<router-link v-if="item.actor_info"
|
<!-- TODO -->
|
||||||
|
<router-link v-if="item.account"
|
||||||
:to="{ name: 'profile',
|
: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">
|
<span class="post-author">
|
||||||
{{ userDisplayName(item.actor_info) }}
|
{{ item.account.display_name }}
|
||||||
</span>
|
</span>
|
||||||
<span class="post-author-id">
|
<span class="post-author-id">
|
||||||
@{{ item.actor_info.account }}
|
@{{ item.account.username }}
|
||||||
</span>
|
</span>
|
||||||
</router-link>
|
</router-link>
|
||||||
<a v-else :href="item.attributedTo">
|
<a v-else :href="item.account.id">
|
||||||
<span class="post-author-id">
|
<span class="post-author-id">
|
||||||
{{ item.attributedTo }}
|
{{ item.account.id }}
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</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 }}
|
{{ relativeTimestamp }}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||||
<div v-if="item.content" class="post-message">
|
<div v-if="item.content" class="post-message">
|
||||||
<MessageContent :source="source" />
|
<MessageContent :item="item" />
|
||||||
</div>
|
</div>
|
||||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
<!-- 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">
|
<div v-if="hasAttachments" class="post-attachments">
|
||||||
<PostAttachment :attachments="item.attachment" />
|
<!-- TODO: clean media_attachments -->
|
||||||
|
<PostAttachment :attachments="item.media_attachments || []" />
|
||||||
</div>
|
</div>
|
||||||
<div v-if="$route && $route.params.type !== 'notifications' && !serverData.public" class="post-actions">
|
<div v-if="$route && $route.params.type !== 'notifications' && !serverData.public" class="post-actions">
|
||||||
<NcButton v-tooltip="t('social', 'Reply')"
|
<NcButton v-tooltip="t('social', 'Reply')"
|
||||||
|
@ -64,7 +69,7 @@
|
||||||
</template>
|
</template>
|
||||||
</NcButton>
|
</NcButton>
|
||||||
<NcActions>
|
<NcActions>
|
||||||
<NcActionButton v-if="item.actor_info.account === cloudId"
|
<NcActionButton v-if="item.account !== undefined && item.account.acct === currentAccount.acct"
|
||||||
icon="icon-delete"
|
icon="icon-delete"
|
||||||
@click="remove()">
|
@click="remove()">
|
||||||
{{ t('social', 'Delete') }}
|
{{ 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 HeartOutline from 'vue-material-design-icons/HeartOutline.vue'
|
||||||
import logger from '../services/logger.js'
|
import logger from '../services/logger.js'
|
||||||
import moment from '@nextcloud/moment'
|
import moment from '@nextcloud/moment'
|
||||||
import { generateUrl } from '@nextcloud/router'
|
|
||||||
import MessageContent from './MessageContent.js'
|
import MessageContent from './MessageContent.js'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
@ -108,47 +112,75 @@ export default {
|
||||||
},
|
},
|
||||||
mixins: [currentUser],
|
mixins: [currentUser],
|
||||||
props: {
|
props: {
|
||||||
item: { type: Object, default: () => {} },
|
/** @type {import('vue').PropType<import('../types/Mastodon.js').Status>} */
|
||||||
parentAnnounce: { type: Object, default: () => {} },
|
item: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {},
|
||||||
|
},
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
/**
|
||||||
|
* @return {string}
|
||||||
|
*/
|
||||||
relativeTimestamp() {
|
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() {
|
timestamp() {
|
||||||
return Date.parse(this.item.published)
|
return Date.parse(this.item.created_at)
|
||||||
},
|
|
||||||
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 {boolean}
|
||||||
|
*/
|
||||||
hasAttachments() {
|
hasAttachments() {
|
||||||
return (typeof this.item.attachment !== 'undefined')
|
// TODO: clean media_attachments
|
||||||
|
return (this.item.media_attachments || []).length > 0
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* @return {boolean}
|
||||||
|
*/
|
||||||
isBoosted() {
|
isBoosted() {
|
||||||
if (typeof this.item.action === 'undefined') {
|
return this.item.reblogged === true
|
||||||
return false
|
|
||||||
}
|
|
||||||
return !!this.item.action.values.boosted
|
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* @return {boolean}
|
||||||
|
*/
|
||||||
|
|
||||||
isLiked() {
|
isLiked() {
|
||||||
if (typeof this.item.action === 'undefined') {
|
return this.item.favourited === true
|
||||||
return false
|
|
||||||
}
|
|
||||||
return !!this.item.action.values.liked
|
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* @return {object}
|
||||||
|
*/
|
||||||
richParameters() {
|
richParameters() {
|
||||||
return {}
|
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: {
|
methods: {
|
||||||
/**
|
/**
|
||||||
|
@ -158,21 +190,22 @@ export default {
|
||||||
*/
|
*/
|
||||||
getSinglePostTimeline(e) {
|
getSinglePostTimeline(e) {
|
||||||
// Display internal or external post
|
// Display internal or external post
|
||||||
if (!this.item.local) {
|
if (!this.isLocal) {
|
||||||
if (this.item.type === 'Note') {
|
if (this.type === 'Note') {
|
||||||
window.open(this.item.id)
|
window.open(this.item.id)
|
||||||
} else if (this.item.type === 'Announce') {
|
} else if (this.type === 'Announce') {
|
||||||
|
// TODO
|
||||||
window.open(this.item.object)
|
window.open(this.item.object)
|
||||||
} else {
|
} 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 {
|
} else {
|
||||||
this.$router.push({
|
this.$router.push({
|
||||||
name: 'single-post',
|
name: 'single-post',
|
||||||
params: {
|
params: {
|
||||||
account: this.item.actor_info.preferredUsername,
|
account: this.item.account.display_name,
|
||||||
id: this.item.id,
|
id: this.item.id,
|
||||||
localId: this.item.id.split('/')[this.item.id.split('/').length - 1],
|
localId: this.item.uri.split('/').pop(),
|
||||||
type: 'single-post',
|
type: 'single-post',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
@ -188,7 +221,7 @@ export default {
|
||||||
boost() {
|
boost() {
|
||||||
const params = {
|
const params = {
|
||||||
post: this.item,
|
post: this.item,
|
||||||
parentAnnounce: this.parentAnnounce,
|
parentAnnounce: this.reblog,
|
||||||
}
|
}
|
||||||
if (this.isBoosted) {
|
if (this.isBoosted) {
|
||||||
this.$store.dispatch('postUnBoost', params)
|
this.$store.dispatch('postUnBoost', params)
|
||||||
|
@ -202,7 +235,7 @@ export default {
|
||||||
like() {
|
like() {
|
||||||
const params = {
|
const params = {
|
||||||
post: this.item,
|
post: this.item,
|
||||||
parentAnnounce: this.parentAnnounce,
|
parentAnnounce: this.reblog,
|
||||||
}
|
}
|
||||||
if (this.isLiked) {
|
if (this.isLiked) {
|
||||||
this.$store.dispatch('postUnlike', params)
|
this.$store.dispatch('postUnlike', params)
|
||||||
|
|
|
@ -24,19 +24,19 @@
|
||||||
<div v-if="item" class="user-entry">
|
<div v-if="item" class="user-entry">
|
||||||
<div class="entry-content">
|
<div class="entry-content">
|
||||||
<div class="user-avatar">
|
<div class="user-avatar">
|
||||||
<NcAvatar v-if="item.local"
|
<NcAvatar v-if="isLocal"
|
||||||
:size="32"
|
:size="32"
|
||||||
:user="item.preferredUsername"
|
:user="item.username"
|
||||||
:disable-tooltip="true" />
|
:disable-tooltip="true" />
|
||||||
<NcAvatar v-else :url="avatarUrl" />
|
<NcAvatar v-else :url="item.avatar" />
|
||||||
</div>
|
</div>
|
||||||
<div class="user-details">
|
<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">
|
<span class="post-author">
|
||||||
{{ item.name }}
|
{{ item.display_name }}
|
||||||
</span>
|
</span>
|
||||||
<span class="user-description">
|
<span class="user-description">
|
||||||
{{ item.account }}
|
{{ item.acct }}
|
||||||
</span>
|
</span>
|
||||||
</router-link>
|
</router-link>
|
||||||
<a v-else
|
<a v-else
|
||||||
|
@ -44,26 +44,24 @@
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer">
|
rel="noreferrer">
|
||||||
<span class="post-author">
|
<span class="post-author">
|
||||||
{{ item.name }}
|
{{ item.display_name }}
|
||||||
</span>
|
</span>
|
||||||
<span class="user-description">
|
<span class="user-description">
|
||||||
{{ item.account }}
|
{{ item.acct }}
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||||
<p v-html="item.summary" />
|
<p v-html="item.note" />
|
||||||
</div>
|
</div>
|
||||||
<FollowButton :account="item.account" :uid="cloudId" />
|
<FollowButton :account="item.acct" :uid="cloudId" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import NcAvatar from '@nextcloud/vue/dist/Components/NcAvatar.js'
|
import NcAvatar from '@nextcloud/vue/dist/Components/NcAvatar.js'
|
||||||
import follow from '../mixins/follow.js'
|
|
||||||
import currentUser from '../mixins/currentUserMixin.js'
|
import currentUser from '../mixins/currentUserMixin.js'
|
||||||
import FollowButton from './FollowButton.vue'
|
import FollowButton from './FollowButton.vue'
|
||||||
import { generateUrl } from '@nextcloud/router'
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'UserEntry',
|
name: 'UserEntry',
|
||||||
|
@ -72,11 +70,14 @@ export default {
|
||||||
NcAvatar,
|
NcAvatar,
|
||||||
},
|
},
|
||||||
mixins: [
|
mixins: [
|
||||||
follow,
|
|
||||||
currentUser,
|
currentUser,
|
||||||
],
|
],
|
||||||
props: {
|
props: {
|
||||||
item: { type: Object, default: () => {} },
|
/** @type {import('vue').PropType<import('../types/Mastodon.js').Account>} */
|
||||||
|
item: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
@ -84,14 +85,11 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
id() {
|
/**
|
||||||
if (this.item.actor_info) {
|
* @return {boolean}
|
||||||
return this.item.actor_info.id
|
*/
|
||||||
}
|
isLocal() {
|
||||||
return this.item.id
|
return !this.item.acct.includes('@')
|
||||||
},
|
|
||||||
avatarUrl() {
|
|
||||||
return generateUrl('/apps/social/api/v1/global/actor/avatar?id=' + this.id)
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 store from './store/index.js'
|
||||||
import router from './router.js'
|
import router from './router.js'
|
||||||
import vuetwemoji from 'vue-twemoji'
|
import vuetwemoji from 'vue-twemoji'
|
||||||
import contenteditableDirective from 'vue-contenteditable-directive'
|
|
||||||
import ClickOutside from 'vue-click-outside'
|
import ClickOutside from 'vue-click-outside'
|
||||||
import VTooltip from '@nextcloud/vue/dist/Directives/Tooltip.js'
|
import VTooltip from '@nextcloud/vue/dist/Directives/Tooltip.js'
|
||||||
|
import VueMasonry from 'vue-masonry-css'
|
||||||
|
|
||||||
sync(store, router)
|
sync(store, router)
|
||||||
|
|
||||||
|
@ -49,13 +49,13 @@ Vue.prototype.OCA = OCA
|
||||||
|
|
||||||
Vue.directive('ClickOutside', ClickOutside)
|
Vue.directive('ClickOutside', ClickOutside)
|
||||||
Vue.directive('Tooltip', VTooltip)
|
Vue.directive('Tooltip', VTooltip)
|
||||||
Vue.use(contenteditableDirective)
|
|
||||||
Vue.use(vuetwemoji, {
|
Vue.use(vuetwemoji, {
|
||||||
baseUrl: OC.linkTo('social', 'img/'), // can set to local folder of emojis. default: https://twemoji.maxcdn.com/
|
baseUrl: OC.linkTo('social', 'img/'), // can set to local folder of emojis. default: https://twemoji.maxcdn.com/
|
||||||
extension: '.svg', // .svg, .png
|
extension: '.svg', // .svg, .png
|
||||||
className: 'emoji', // custom className for image output
|
className: 'emoji', // custom className for image output
|
||||||
size: 'twemoji', // image size
|
size: 'twemoji', // image size
|
||||||
})
|
})
|
||||||
|
Vue.use(VueMasonry)
|
||||||
|
|
||||||
/* eslint-disable-next-line no-new */
|
/* eslint-disable-next-line no-new */
|
||||||
new Vue({
|
new Vue({
|
||||||
|
|
|
@ -36,7 +36,7 @@ export default {
|
||||||
return (this.uid.indexOf('@') === -1) ? this.uid + '@' + this.hostname : this.uid
|
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() {
|
accountInfo() {
|
||||||
return this.$store.getters.getAccount(this.profileAccount)
|
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
|
* 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
|
* where components would first show "user not found" before display an account's account info
|
||||||
|
*
|
||||||
|
* @return {boolean}
|
||||||
*/
|
*/
|
||||||
accountLoaded() {
|
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>
|
* @copyright Copyright (c) 2018 Julius Härtl <jus@bitgrid.net>
|
||||||
*
|
*
|
||||||
* @author 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'
|
import serverData from './serverData.js'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
mixins: [
|
mixins: [
|
||||||
serverData,
|
serverData,
|
||||||
],
|
],
|
||||||
computed: {
|
computed: {
|
||||||
currentUser() {
|
currentUser() {
|
||||||
return OC.getCurrentUser()
|
return getCurrentUser()
|
||||||
},
|
},
|
||||||
socialId() {
|
socialId() {
|
||||||
return '@' + this.cloudId
|
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>
|
* @copyright Copyright (c) 2019 Julius Härtl <jus@bitgrid.net>
|
||||||
*
|
*
|
||||||
* @author Julius Härtl <jus@bitgrid.net>
|
* @author Julius Härtl <jus@bitgrid.net>
|
||||||
|
|
|
@ -39,7 +39,7 @@
|
||||||
export default {
|
export default {
|
||||||
computed: {
|
computed: {
|
||||||
/**
|
/**
|
||||||
* @return {Partial<ServerData>} Returns the serverData object
|
* @return {ServerData} Returns the serverData object
|
||||||
*/
|
*/
|
||||||
serverData() {
|
serverData() {
|
||||||
if (!this.$store) {
|
if (!this.$store) {
|
||||||
|
@ -47,6 +47,9 @@ export default {
|
||||||
}
|
}
|
||||||
return this.$store.getters.getServerData
|
return this.$store.getters.getServerData
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* @return {string}
|
||||||
|
*/
|
||||||
hostname() {
|
hostname() {
|
||||||
const url = document.createElement('a')
|
const url = document.createElement('a')
|
||||||
url.setAttribute('href', this.serverData.cloudAddress)
|
url.setAttribute('href', this.serverData.cloudAddress)
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
/*
|
/**
|
||||||
* @copyright Copyright (c) 2019 Julius Härtl <jus@bitgrid.net>
|
* @copyright Copyright (c) 2019 Julius Härtl <jus@bitgrid.net>
|
||||||
*
|
*
|
||||||
* @author 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',
|
path: '/:index(index.php/)?apps/social/@:account',
|
||||||
components: {
|
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',
|
path: '/:index(index.php/)?apps/social/ostatus/follow',
|
||||||
components: {
|
components: {
|
||||||
|
|
|
@ -23,162 +23,234 @@
|
||||||
import axios from '@nextcloud/axios'
|
import axios from '@nextcloud/axios'
|
||||||
import { set } from 'vue'
|
import { set } from 'vue'
|
||||||
import { generateUrl } from '@nextcloud/router'
|
import { generateUrl } from '@nextcloud/router'
|
||||||
|
import { showError } from '@nextcloud/dialogs'
|
||||||
|
import logger from '../services/logger.js'
|
||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
currentAccount: {},
|
currentAccount: '',
|
||||||
|
/** @type {Object<string, import('../types/Mastodon.js').Account>} */
|
||||||
accounts: {},
|
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: {},
|
accountIdMap: {},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {typeof state} state
|
||||||
|
* @param {object} payload
|
||||||
|
* @param {string} payload.actorId
|
||||||
|
* @param {import('../types/Mastodon').Account} payload.data
|
||||||
|
*/
|
||||||
const addAccount = (state, { actorId, data }) => {
|
const addAccount = (state, { actorId, data }) => {
|
||||||
set(state.accounts, actorId, Object.assign({
|
set(state.accounts, actorId, { ...state.accounts[actorId], ...data })
|
||||||
followersList: [],
|
set(state.accountsFollowers, actorId, [])
|
||||||
followingList: [],
|
set(state.accountsFollowings, actorId, [])
|
||||||
details: {
|
const accountId = (data.acct.indexOf('@') === -1) ? data.acct + '@' + new URL(data.url).hostname : data.acct
|
||||||
following: false,
|
set(state.accountIdMap, accountId, data.url)
|
||||||
follower: false,
|
|
||||||
},
|
|
||||||
}, state.accounts[actorId], data))
|
|
||||||
set(state.accountIdMap, data.account, data.id)
|
|
||||||
}
|
}
|
||||||
const _getActorIdForAccount = (account) => state.accountIdMap[account]
|
const _getActorIdForAccount = (account) => state.accountIdMap[account]
|
||||||
|
|
||||||
|
/** @type {import('vuex').MutationTree<state, any>} */
|
||||||
const mutations = {
|
const mutations = {
|
||||||
|
/**
|
||||||
|
* @param state
|
||||||
|
* @param {string} account
|
||||||
|
*/
|
||||||
setCurrentAccount(state, account) {
|
setCurrentAccount(state, account) {
|
||||||
state.currentAccount = 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 }) {
|
||||||
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 }) {
|
addFollowers(state, { account, data }) {
|
||||||
const users = []
|
const users = []
|
||||||
for (const index in data) {
|
for (const actor of data) {
|
||||||
const actor = data[index].actor_info
|
users.push(actor.url)
|
||||||
if (typeof actor !== 'undefined' && account !== actor.account) {
|
addAccount(state, {
|
||||||
users.push(actor.id)
|
actorId: actor.url,
|
||||||
addAccount(state, {
|
data: actor,
|
||||||
actorId: actor.id,
|
})
|
||||||
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 }) {
|
addFollowing(state, { account, data }) {
|
||||||
const users = []
|
const users = []
|
||||||
for (const index in data) {
|
for (const actor of data) {
|
||||||
const actor = data[index].actor_info
|
users.push(actor.url)
|
||||||
if (typeof actor !== 'undefined' && account !== actor.account) {
|
addAccount(state, {
|
||||||
users.push(actor.id)
|
actorId: actor.url,
|
||||||
addAccount(state, {
|
data: actor,
|
||||||
actorId: actor.id,
|
})
|
||||||
data: actor,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
set(state.accounts[_getActorIdForAccount(account)], 'followingList', users)
|
set(state.accountsFollowings, _getActorIdForAccount(account), users)
|
||||||
},
|
},
|
||||||
followAccount(state, accountToFollow) {
|
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) {
|
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 = {
|
const getters = {
|
||||||
getAllAccounts(state) {
|
getAllAccounts(state) {
|
||||||
return (account) => { return state.accounts }
|
return () => { return state.accounts }
|
||||||
},
|
},
|
||||||
getAccount(state, getters) {
|
getAccount(state, getters) {
|
||||||
return (account) => {
|
return (/** @type {string} */ account) => {
|
||||||
return state.accounts[_getActorIdForAccount(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) {
|
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) {
|
accountLoaded(state) {
|
||||||
return (account) => state.accounts[_getActorIdForAccount(account)]
|
return (/** @type {string} */ account) => state.accounts[_getActorIdForAccount(account)]
|
||||||
},
|
},
|
||||||
getAccountFollowers(state) {
|
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) {
|
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() {
|
getActorIdForAccount() {
|
||||||
return _getActorIdForAccount
|
return _getActorIdForAccount
|
||||||
},
|
},
|
||||||
isFollowingUser(state) {
|
isFollowingUser(state) {
|
||||||
return (followingAccount) => {
|
return (/** @type {string} */ followingAccount) => state.accountsRelationships[_getActorIdForAccount(followingAccount)]?.following || false
|
||||||
const account = state.accounts[_getActorIdForAccount(followingAccount)]
|
|
||||||
return account && account.details ? account.details.following : false
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @type {import('vuex').ActionTree<state, any>} */
|
||||||
const actions = {
|
const actions = {
|
||||||
fetchAccountInfo(context, account) {
|
async fetchAccountInfo(context, account) {
|
||||||
return axios.get(generateUrl(`apps/social/api/v1/global/account/info?account=${account}`)).then((response) => {
|
try {
|
||||||
context.commit('addAccount', { actorId: response.data.result.account.id, data: response.data.result.account })
|
const response = await axios.get(generateUrl(`apps/social/api/v1/global/account/info?account=${account}`))
|
||||||
return response.data.result.account
|
context.commit('addAccount', { actorId: response.data.url, data: response.data })
|
||||||
}).catch(() => {
|
return response.data
|
||||||
OC.Notification.showTemporary(`Failed to load account details ${account}`)
|
} catch (error) {
|
||||||
})
|
logger.error('Failed to load local account details', { error })
|
||||||
|
showError(`Failed to load local account details ${account}`)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
fetchPublicAccountInfo(context, uid) {
|
async fetchAccountRelationshipInfo(context, ids) {
|
||||||
return axios.get(generateUrl(`apps/social/api/v1/account/${uid}/info`)).then((response) => {
|
try {
|
||||||
context.commit('addAccount', { actorId: response.data.result.account.id, data: response.data.result.account })
|
const response = await axios.get(generateUrl('apps/social/api/v1/accounts/relationships'), { params: { id: ids } })
|
||||||
return response.data.result.account
|
response.data.forEach(account => context.commit('addRelationship', { actorId: account.id, data: account }))
|
||||||
}).catch(() => {
|
return response.data
|
||||||
OC.Notification.showTemporary(`Failed to load account details ${uid}`)
|
} 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) {
|
fetchCurrentAccountInfo({ commit, dispatch }, account) {
|
||||||
commit('setCurrentAccount', account)
|
commit('setCurrentAccount', account)
|
||||||
dispatch('fetchAccountInfo', account)
|
dispatch('fetchAccountInfo', account)
|
||||||
},
|
},
|
||||||
followAccount(context, { currentAccount, accountToFollow }) {
|
async followAccount(context, { currentAccount, accountToFollow }) {
|
||||||
return axios.put(generateUrl('/apps/social/api/v1/current/follow?account=' + accountToFollow)).then((response) => {
|
try {
|
||||||
|
const response = await axios.put(generateUrl('/apps/social/api/v1/current/follow?account=' + accountToFollow))
|
||||||
if (response.data.status === -1) {
|
if (response.data.status === -1) {
|
||||||
return Promise.reject(response)
|
return Promise.reject(response)
|
||||||
}
|
}
|
||||||
context.commit('followAccount', accountToFollow)
|
context.commit('followAccount', accountToFollow)
|
||||||
return Promise.resolve(response)
|
return response
|
||||||
}).catch((error) => {
|
} catch (error) {
|
||||||
OC.Notification.showTemporary(`Failed to follow user ${accountToFollow}`)
|
showError(`Failed to follow user ${accountToFollow}`)
|
||||||
console.error(`Failed to follow user ${accountToFollow}`, error)
|
logger.error(`Failed to follow user ${accountToFollow}`, { error })
|
||||||
})
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
unfollowAccount(context, { currentAccount, accountToUnfollow }) {
|
async unfollowAccount(context, { currentAccount, accountToUnfollow }) {
|
||||||
return axios.delete(generateUrl('/apps/social/api/v1/current/follow?account=' + accountToUnfollow)).then((response) => {
|
try {
|
||||||
|
const response = await axios.delete(generateUrl('/apps/social/api/v1/current/follow?account=' + accountToUnfollow))
|
||||||
if (response.data.status === -1) {
|
if (response.data.status === -1) {
|
||||||
return Promise.reject(response)
|
return Promise.reject(response)
|
||||||
}
|
}
|
||||||
context.commit('unfollowAccount', accountToUnfollow)
|
context.commit('unfollowAccount', accountToUnfollow)
|
||||||
return Promise.resolve(response)
|
return response
|
||||||
}).catch((error) => {
|
} catch (error) {
|
||||||
OC.Notification.showTemporary(`Failed to unfollow user ${accountToUnfollow}`)
|
showError(`Failed to unfollow user ${accountToUnfollow}`)
|
||||||
console.error(`Failed to unfollow user ${accountToUnfollow}`, error.response.data)
|
logger.error(`Failed to unfollow user ${accountToUnfollow}`, { error })
|
||||||
return Promise.reject(error.response.data)
|
return error
|
||||||
})
|
}
|
||||||
},
|
},
|
||||||
fetchAccountFollowers(context, account) {
|
async fetchAccountFollowers(context, account) {
|
||||||
// TODO: fetching followers/following information of remotes is currently not supported
|
// TODO: fetching followers/following information of remotes is currently not supported
|
||||||
const parts = account.split('@')
|
const parts = account.split('@')
|
||||||
const uid = (parts.length === 2 ? parts[0] : account)
|
const uid = (parts.length === 2 ? parts[0] : account)
|
||||||
axios.get(generateUrl(`apps/social/api/v1/account/${uid}/followers`)).then((response) => {
|
try {
|
||||||
context.commit('addFollowers', { account, data: response.data.result })
|
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
|
// TODO: fetching followers/following information of remotes is currently not supported
|
||||||
const parts = account.split('@')
|
const parts = account.split('@')
|
||||||
const uid = (parts.length === 2 ? parts[0] : account)
|
const uid = (parts.length === 2 ? parts[0] : account)
|
||||||
axios.get(generateUrl(`apps/social/api/v1/account/${uid}/following`)).then((response) => {
|
try {
|
||||||
context.commit('addFollowing', { account, data: response.data.result })
|
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 Vue from 'vue'
|
||||||
import { generateUrl } from '@nextcloud/router'
|
|
||||||
|
|
||||||
/**
|
import axios from '@nextcloud/axios'
|
||||||
* @property {object} timeline - The posts' collection
|
import { generateUrl } from '@nextcloud/router'
|
||||||
* @property {number} since - Time (EPOCH) of the most recent post
|
import { showError } from '@nextcloud/dialogs'
|
||||||
* @property {string} type - Timeline's type: 'home', 'single-post',...
|
|
||||||
* @property {object} params - Timeline's parameters
|
import logger from '../services/logger.js'
|
||||||
* @property {string} account -
|
|
||||||
*/
|
|
||||||
const state = {
|
const state = {
|
||||||
|
/**
|
||||||
|
* @type {Object<string, import('../types/Mastodon.js').Status>} timeline - The posts' collection
|
||||||
|
*/
|
||||||
timeline: {},
|
timeline: {},
|
||||||
since: Math.floor(Date.now() / 1000) + 1,
|
/**
|
||||||
|
* @type {string} type - Timeline's type: 'home', 'single-post',...
|
||||||
|
*/
|
||||||
type: 'home',
|
type: 'home',
|
||||||
/**
|
/**
|
||||||
* @namespace params
|
* @type {object} params - Timeline's parameters
|
||||||
* @property {string} account ???
|
* @property {string} params.account ???
|
||||||
* @property {string} id
|
* @property {string} params.id
|
||||||
* @property {string} localId
|
* @property {string} params.localId
|
||||||
* @property {string} type ???
|
* @property {string} params.type ???
|
||||||
*/
|
*/
|
||||||
params: {},
|
params: {},
|
||||||
|
/**
|
||||||
|
* @type {string} account -
|
||||||
|
*/
|
||||||
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.
|
* It's up to the view to honor this status or not.
|
||||||
|
*
|
||||||
* @member {boolean}
|
* @member {boolean}
|
||||||
*/
|
*/
|
||||||
composerDisplayStatus: false,
|
composerDisplayStatus: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @type {import('vuex').MutationTree<state>} */
|
||||||
const mutations = {
|
const mutations = {
|
||||||
|
/**
|
||||||
|
* @param state
|
||||||
|
* @param {import('../types/Mastodon.js').Status[]} data
|
||||||
|
*/
|
||||||
addToTimeline(state, data) {
|
addToTimeline(state, data) {
|
||||||
for (const item in data) {
|
// TODO: fix to handle ancestors
|
||||||
state.since = data[item].publishedTime
|
if (data.descendants) {
|
||||||
Vue.set(state.timeline, data[item].id, data[item])
|
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) {
|
removePost(state, post) {
|
||||||
Vue.delete(state.timeline, post.id)
|
Vue.delete(state.timeline, post.id)
|
||||||
},
|
},
|
||||||
resetTimeline(state) {
|
resetTimeline(state) {
|
||||||
state.timeline = {}
|
state.timeline = {}
|
||||||
state.since = Math.floor(Date.now() / 1000) + 1
|
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* @param state
|
||||||
|
* @param {string} type
|
||||||
|
*/
|
||||||
setTimelineType(state, type) {
|
setTimelineType(state, type) {
|
||||||
state.type = type
|
state.type = type
|
||||||
},
|
},
|
||||||
setTimelineParams(state, params) {
|
setTimelineParams(state, params) {
|
||||||
state.params = params
|
state.params = params
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* @param state
|
||||||
|
* @param {boolean} status
|
||||||
|
*/
|
||||||
setComposerDisplayStatus(state, status) {
|
setComposerDisplayStatus(state, status) {
|
||||||
state.composerDisplayStatus = status
|
state.composerDisplayStatus = status
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* @param state
|
||||||
|
* @param {string} account
|
||||||
|
*/
|
||||||
setAccount(state, account) {
|
setAccount(state, account) {
|
||||||
state.account = 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 }) {
|
likePost(state, { post, parentAnnounce }) {
|
||||||
if (typeof state.timeline[post.id] !== 'undefined') {
|
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') {
|
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 }) {
|
unlikePost(state, { post, parentAnnounce }) {
|
||||||
if (typeof state.timeline[post.id] !== 'undefined') {
|
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') {
|
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 }) {
|
boostPost(state, { post, parentAnnounce }) {
|
||||||
if (typeof state.timeline[post.id] !== 'undefined') {
|
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') {
|
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 }) {
|
unboostPost(state, { post, parentAnnounce }) {
|
||||||
if (typeof state.timeline[post.id] !== 'undefined') {
|
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') {
|
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 = {
|
const getters = {
|
||||||
getComposerDisplayStatus(state) {
|
getComposerDisplayStatus(state) {
|
||||||
return state.composerDisplayStatus
|
return state.composerDisplayStatus
|
||||||
},
|
},
|
||||||
getTimeline(state) {
|
getTimeline(state) {
|
||||||
return Object.values(state.timeline).sort(function(a, b) {
|
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) {
|
getPostFromTimeline(state) {
|
||||||
|
@ -132,6 +186,8 @@ const getters = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @type {import('vuex').ActionTree<state, any>} */
|
||||||
const actions = {
|
const actions = {
|
||||||
changeTimelineType(context, { type, params }) {
|
changeTimelineType(context, { type, params }) {
|
||||||
context.commit('resetTimeline')
|
context.commit('resetTimeline')
|
||||||
|
@ -144,108 +200,183 @@ const actions = {
|
||||||
context.commit('setTimelineType', 'account')
|
context.commit('setTimelineType', 'account')
|
||||||
context.commit('setAccount', account)
|
context.commit('setAccount', account)
|
||||||
},
|
},
|
||||||
async post(context, post) {
|
/**
|
||||||
|
* @param context
|
||||||
|
* @param {File} file
|
||||||
|
*/
|
||||||
|
async createMedia(context, file) {
|
||||||
try {
|
try {
|
||||||
const { data } = await axios.post(generateUrl('apps/social/api/v1/post'), post, {
|
const formData = new FormData()
|
||||||
headers: {
|
formData.append('file', file)
|
||||||
'Content-Type': 'multipart/form-data',
|
const { data } = await axios.post(
|
||||||
},
|
generateUrl('apps/social/api/v1/media'),
|
||||||
})
|
formData,
|
||||||
logger.info('Post created with token ' + data.result.token)
|
{
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
logger.info('Media created with id ' + data.id)
|
||||||
|
return data
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
OC.Notification.showTemporary('Failed to create a post')
|
showError('Failed to create a media')
|
||||||
logger.error('Failed to create a post', { error: error.response })
|
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) {
|
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)
|
context.commit('removePost', post)
|
||||||
logger.info('Post deleted with token ' + response.data.result.token)
|
logger.info('Post deleted with token ' + response.data.result.token)
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
OC.Notification.showTemporary('Failed to delete the post')
|
showError('Failed to delete the post')
|
||||||
logger.error('Failed to delete the post', { error })
|
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 }) {
|
postLike(context, { post, parentAnnounce }) {
|
||||||
return new Promise((resolve, reject) => {
|
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 })
|
context.commit('likePost', { post, parentAnnounce })
|
||||||
resolve(response)
|
resolve(response)
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
OC.Notification.showTemporary('Failed to like post')
|
showError('Failed to like post')
|
||||||
logger.error('Failed to like post', { error: error.response })
|
logger.error('Failed to like post', { error })
|
||||||
reject(error)
|
reject(error)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* @param context
|
||||||
|
* @param {object} root0
|
||||||
|
* @param {import('../types/Mastodon.js').Status} root0.post
|
||||||
|
* @param {object} root0.parentAnnounce
|
||||||
|
*/
|
||||||
postUnlike(context, { post, 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 })
|
context.commit('unlikePost', { post, parentAnnounce })
|
||||||
// Remove post from list if we are in the 'liked' timeline
|
// Remove post from list if we are in the 'liked' timeline
|
||||||
if (state.type === 'liked') {
|
if (state.type === 'liked') {
|
||||||
context.commit('removePost', post)
|
context.commit('removePost', post)
|
||||||
}
|
}
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
OC.Notification.showTemporary('Failed to unlike post')
|
showError('Failed to unlike post')
|
||||||
logger.error('Failed to unlike post', { error })
|
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 }) {
|
postBoost(context, { post, parentAnnounce }) {
|
||||||
return new Promise((resolve, reject) => {
|
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 })
|
context.commit('boostPost', { post, parentAnnounce })
|
||||||
logger.info('Post boosted with token ' + response.data.result.token)
|
logger.info('Post boosted with token ' + response.data.result.token)
|
||||||
resolve(response)
|
resolve(response)
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
OC.Notification.showTemporary('Failed to create a boost post')
|
showError('Failed to create a boost post')
|
||||||
logger.error('Failed to create a boost post', { error: error.response })
|
logger.error('Failed to create a boost post', { error })
|
||||||
reject(error)
|
reject(error)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* @param context
|
||||||
|
* @param {object} root0
|
||||||
|
* @param {import('../types/Mastodon.js').Status} root0.post
|
||||||
|
* @param {object} root0.parentAnnounce
|
||||||
|
*/
|
||||||
postUnBoost(context, { post, 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 })
|
context.commit('unboostPost', { post, parentAnnounce })
|
||||||
logger.info('Boost deleted with token ' + response.data.result.token)
|
logger.info('Boost deleted with token ' + response.data.result.token)
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
OC.Notification.showTemporary('Failed to delete the boost')
|
showError('Failed to delete the boost')
|
||||||
logger.error('Failed to delete the boost', { error })
|
logger.error('Failed to delete the boost', { error })
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
refreshTimeline(context) {
|
refreshTimeline(context) {
|
||||||
return this.dispatch('fetchTimeline', { sinceTimestamp: Math.floor(Date.now() / 1000) + 1 })
|
return this.dispatch('fetchTimeline')
|
||||||
},
|
},
|
||||||
fetchTimeline(context, { sinceTimestamp }) {
|
/**
|
||||||
|
*
|
||||||
if (typeof sinceTimestamp === 'undefined') {
|
* @param {object} context
|
||||||
sinceTimestamp = state.since - 1
|
* @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 = ''
|
let url = ''
|
||||||
if (state.type === 'account') {
|
switch (state.type) {
|
||||||
url = generateUrl(`apps/social/api/v1/account/${state.account}/stream?limit=25&since=` + sinceTimestamp)
|
case 'account':
|
||||||
} else if (state.type === 'tags') {
|
url = generateUrl(`apps/social/api/v1/accounts/${state.account}/statuses`)
|
||||||
url = generateUrl(`apps/social/api/v1/stream/tag/${state.params.tag}?limit=25&since=` + sinceTimestamp)
|
break
|
||||||
} else if (state.type === 'single-post') {
|
case 'tags':
|
||||||
url = generateUrl(`apps/social/local/v1/post/replies?id=${state.params.id}&limit=5&since=` + sinceTimestamp)
|
url = generateUrl(`apps/social/api/v1/timelines/tag/${state.params.tag}`)
|
||||||
} else {
|
break
|
||||||
url = generateUrl(`apps/social/api/v1/stream/${state.type}?limit=25&since=` + sinceTimestamp)
|
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
|
// 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) {
|
// Add results to timeline
|
||||||
throw response.message
|
context.commit('addToTimeline', response.data)
|
||||||
}
|
|
||||||
|
|
||||||
// Add results to timeline
|
return response.data
|
||||||
context.commit('addToTimeline', response.data.result)
|
|
||||||
|
|
||||||
return response.data
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* @param context
|
||||||
|
* @param {import('../types/Mastodon.js').Status[]} data
|
||||||
|
*/
|
||||||
addToTimeline(context, data) {
|
addToTimeline(context, data) {
|
||||||
context.commit('addToTimeline', 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: {
|
computed: {
|
||||||
|
/** @return {object[]} */
|
||||||
items() {
|
items() {
|
||||||
return this.notifications.map((n) => {
|
return this.notifications.map((n) => {
|
||||||
return {
|
return {
|
||||||
|
@ -88,11 +89,13 @@ export default {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
/** @return {number} */
|
||||||
lastTimestamp() {
|
lastTimestamp() {
|
||||||
return this.notifications.length
|
return this.notifications.length
|
||||||
? this.notifications[0].publishedTime
|
? this.notifications[0].publishedTime
|
||||||
: 0
|
: 0
|
||||||
},
|
},
|
||||||
|
/** @return {string} */
|
||||||
emptyContentMessage() {
|
emptyContentMessage() {
|
||||||
if (this.state === 'error') {
|
if (this.state === 'error') {
|
||||||
return t('social', 'Error getting Social notifications')
|
return t('social', 'Error getting Social notifications')
|
||||||
|
@ -101,6 +104,7 @@ export default {
|
||||||
}
|
}
|
||||||
return ''
|
return ''
|
||||||
},
|
},
|
||||||
|
/** @return {string} */
|
||||||
emptyContentIcon() {
|
emptyContentIcon() {
|
||||||
if (this.state === 'error') {
|
if (this.state === 'error') {
|
||||||
return 'icon-close'
|
return 'icon-close'
|
||||||
|
|
|
@ -24,7 +24,7 @@
|
||||||
<div :class="{'icon-loading': !accountLoaded}" class="social__wrapper">
|
<div :class="{'icon-loading': !accountLoaded}" class="social__wrapper">
|
||||||
<ProfileInfo v-if="accountLoaded && accountInfo" :uid="uid" />
|
<ProfileInfo v-if="accountLoaded && accountInfo" :uid="uid" />
|
||||||
<!-- TODO: we have no details, timeline and follower list for non-local accounts for now -->
|
<!-- 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"
|
<NcEmptyContent v-if="accountLoaded && !accountInfo"
|
||||||
:title="t('social', 'User not found')"
|
:title="t('social', 'User not found')"
|
||||||
:description="t('social', 'Sorry, we could not find the account of {userId}', { userId: uid })">
|
:description="t('social', 'Sorry, we could not find the account of {userId}', { userId: uid })">
|
||||||
|
@ -57,19 +57,22 @@ export default {
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
state: [],
|
state: [],
|
||||||
|
/** @type {string|null} */
|
||||||
uid: null,
|
uid: null,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
/** @return {import('../types/Mastodon').Status[]} */
|
||||||
timeline() {
|
timeline() {
|
||||||
return this.$store.getters.getTimeline
|
return this.$store.getters.getTimeline
|
||||||
},
|
},
|
||||||
|
/** @return {string} */
|
||||||
emptyContentImage() {
|
emptyContentImage() {
|
||||||
return generateFilePath('social', 'img', 'undraw/profile.svg')
|
return generateFilePath('social', 'img', 'undraw/profile.svg')
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// Start fetching account information before mounting the component
|
// Start fetching account information before mounting the component
|
||||||
beforeMount() {
|
async beforeMount() {
|
||||||
this.uid = this.$route.params.account || this.serverData.account
|
this.uid = this.$route.params.account || this.serverData.account
|
||||||
|
|
||||||
// Are we authenticated?
|
// 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,
|
// 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
|
// 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) => {
|
/** @type {[import('../types/Mastodon').Account]} */
|
||||||
this.uid = response.account
|
const response = await this.$store.dispatch(fetchMethod, this.profileAccount)
|
||||||
})
|
this.uid = response.acct
|
||||||
|
await this.$store.dispatch('fetchAccountRelationshipInfo', [this.accountInfo.id])
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -39,9 +39,11 @@ export default {
|
||||||
serverData,
|
serverData,
|
||||||
],
|
],
|
||||||
computed: {
|
computed: {
|
||||||
|
/** @return {string} */
|
||||||
profileAccount() {
|
profileAccount() {
|
||||||
return (this.$route.params.account.indexOf('@') === -1) ? this.$route.params.account + '@' + this.hostname : this.$route.params.account
|
return (this.$route.params.account.indexOf('@') === -1) ? this.$route.params.account + '@' + this.hostname : this.$route.params.account
|
||||||
},
|
},
|
||||||
|
/** @return {import('../types/Mastodon.js').Account[]} */
|
||||||
users() {
|
users() {
|
||||||
if (this.$route.name === 'profile.followers') {
|
if (this.$route.name === 'profile.followers') {
|
||||||
return this.$store.getters.getAccountFollowers(this.profileAccount)
|
return this.$store.getters.getAccountFollowers(this.profileAccount)
|
||||||
|
|
|
@ -34,7 +34,6 @@
|
||||||
<script>
|
<script>
|
||||||
import Composer from './../components/Composer/Composer.vue'
|
import Composer from './../components/Composer/Composer.vue'
|
||||||
import CurrentUserMixin from './../mixins/currentUserMixin.js'
|
import CurrentUserMixin from './../mixins/currentUserMixin.js'
|
||||||
import follow from './../mixins/follow.js'
|
|
||||||
import TimelineList from './../components/TimelineList.vue'
|
import TimelineList from './../components/TimelineList.vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
@ -45,7 +44,6 @@ export default {
|
||||||
},
|
},
|
||||||
mixins: [
|
mixins: [
|
||||||
CurrentUserMixin,
|
CurrentUserMixin,
|
||||||
follow,
|
|
||||||
],
|
],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
<div class="social__wrapper">
|
<div class="social__wrapper">
|
||||||
<ProfileInfo v-if="accountLoaded && accountInfo" :uid="uid" />
|
<ProfileInfo v-if="accountLoaded && accountInfo" :uid="uid" />
|
||||||
<Composer v-show="composerDisplayStatus" />
|
<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" />
|
<TimelineList v-if="timeline" :type="$route.params.type" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -72,14 +72,14 @@ export default {
|
||||||
this.$store.dispatch(this.serverData.public ? 'fetchPublicAccountInfo' : 'fetchAccountInfo', this.account).then((response) => {
|
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,
|
// 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
|
// 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
|
// Fetch single post timeline
|
||||||
const params = {
|
const params = {
|
||||||
account: this.account,
|
account: this.account,
|
||||||
id: window.location.href,
|
id: window.location.href,
|
||||||
localId: window.location.href.split('/')[window.location.href.split('/').length - 1],
|
localId: this.mainPost.id,
|
||||||
type: 'single-post',
|
type: 'single-post',
|
||||||
}
|
}
|
||||||
this.$store.dispatch('changeTimelineType', {
|
this.$store.dispatch('changeTimelineType', {
|
||||||
|
|
|
@ -1,8 +1,15 @@
|
||||||
// SPDX-FileCopyrigthText: 2022 Carl Schwan <carl@carlschwan.eu>
|
// SPDX-FileCopyrigthText: 2022 Carl Schwan <carl@carlschwan.eu>
|
||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
const path = require('path');
|
const path = require('path')
|
||||||
const webpackConfig = require('@nextcloud/webpack-vue-config')
|
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 = {
|
webpackConfig.entry = {
|
||||||
social: path.join(__dirname, 'src', 'main.js'),
|
social: path.join(__dirname, 'src', 'main.js'),
|
||||||
|
|
Ładowanie…
Reference in New Issue