Let's not support oci yet

Signed-off-by: Carl Schwan <carl@carlschwan.eu>
update-master-target-versions
Carl Schwan 2022-10-27 15:07:12 +02:00
rodzic ffe5f1bacb
commit db3cb63810
44 zmienionych plików z 893 dodań i 1222 usunięć

Wyświetl plik

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

Wyświetl plik

@ -1,142 +0,0 @@
# This workflow is provided via the organization template repository
#
# https://github.com/nextcloud/.github
# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization
name: PHPUnit
on:
pull_request:
paths:
- '.github/workflows/**'
- 'appinfo/**'
- 'lib/**'
- 'templates/**'
- 'tests/**'
- 'vendor/**'
- 'vendor-bin/**'
- '.php-cs-fixer.dist.php'
- 'composer.json'
- 'composer.lock'
push:
branches:
- main
- master
- stable*
permissions:
contents: read
concurrency:
group: phpunit-oci-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
env:
# Location of the phpunit.xml and phpunit.integration.xml files
PHPUNIT_CONFIG: ./tests/phpunit.xml
PHPUNIT_INTEGRATION_CONFIG: ./tests/phpunit.integration.xml
jobs:
phpunit-oci:
runs-on: ubuntu-latest
strategy:
matrix:
php-versions: ['8.0']
server-versions: ['master']
services:
oracle:
image: deepdiver/docker-oracle-xe-11g # 'wnameless/oracle-xe-11g-r2'
ports:
- 1521:1521/tcp
steps:
- name: Set app env
run: |
# Split and keep last
echo "APP_NAME=${GITHUB_REPOSITORY##*/}" >> $GITHUB_ENV
- name: Checkout server
uses: actions/checkout@v3
with:
submodules: true
repository: nextcloud/server
ref: ${{ matrix.server-versions }}
- name: Checkout app
uses: actions/checkout@v3
with:
path: apps/${{ env.APP_NAME }}
- name: Set up php ${{ matrix.php-versions }}
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-versions }}
extensions: mbstring, fileinfo, intl, sqlite, pdo_sqlite, oci8
tools: phpunit
coverage: none
- name: Check composer file existence
id: check_composer
uses: andstor/file-existence-action@v2
with:
files: apps/${{ env.APP_NAME }}/composer.json
- name: Set up PHPUnit
# Only run if phpunit config file exists
if: steps.check_composer.outputs.files_exists == 'true'
working-directory: apps/${{ env.APP_NAME }}
run: composer i
- name: Set up Nextcloud
env:
DB_PORT: 1521
run: |
mkdir data
./occ maintenance:install --verbose --database=oci --database-name=XE --database-host=127.0.0.1 --database-port=$DB_PORT --database-user=autotest --database-pass=owncloud --admin-user admin --admin-pass admin
./occ app:enable --force ${{ env.APP_NAME }}
- name: Check PHPUnit config file existence
id: check_phpunit
uses: andstor/file-existence-action@v2
with:
files: apps/${{ env.APP_NAME }}/${{ env.PHPUNIT_CONFIG }}
- name: PHPUnit
# Only run if phpunit config file exists
if: steps.check_phpunit.outputs.files_exists == 'true'
working-directory: apps/${{ env.APP_NAME }}
run: ./vendor/phpunit/phpunit/phpunit -c ${{ env.PHPUNIT_CONFIG }}
- name: Check PHPUnit integration config file existence
id: check_integration
uses: andstor/file-existence-action@v2
with:
files: apps/${{ env.APP_NAME }}/${{ env.PHPUNIT_INTEGRATION_CONFIG }}
- name: Run Nextcloud
# Only run if phpunit integration config file exists
if: steps.check_integration.outputs.files_exists == 'true'
run: php -S localhost:8080 &
- name: PHPUnit integration
# Only run if phpunit integration config file exists
if: steps.check_integration.outputs.files_exists == 'true'
working-directory: apps/${{ env.APP_NAME }}
run: ./vendor/phpunit/phpunit/phpunit -c ${{ env.PHPUNIT_INTEGRATION_CONFIG }}
summary:
permissions:
contents: none
runs-on: ubuntu-latest
needs: phpunit-oci
if: always()
name: phpunit-oci-summary
steps:
- name: Summary status
run: if ${{ needs.phpunit-oci.result != 'success' }}; then exit 1; fi

2
package-lock.json wygenerowano
Wyświetl plik

@ -11,7 +11,9 @@
"dependencies": { "dependencies": {
"@nextcloud/auth": "^2.0.0", "@nextcloud/auth": "^2.0.0",
"@nextcloud/axios": "^2.0.0", "@nextcloud/axios": "^2.0.0",
"@nextcloud/dialogs": "^3.2.0",
"@nextcloud/initial-state": "^2.0.0", "@nextcloud/initial-state": "^2.0.0",
"@nextcloud/l10n": "^1.6.0",
"@nextcloud/logger": "^2.2.1", "@nextcloud/logger": "^2.2.1",
"@nextcloud/moment": "^1.2.1", "@nextcloud/moment": "^1.2.1",
"@nextcloud/router": "^1.2.0", "@nextcloud/router": "^1.2.0",

Wyświetl plik

@ -30,7 +30,9 @@
"dependencies": { "dependencies": {
"@nextcloud/auth": "^2.0.0", "@nextcloud/auth": "^2.0.0",
"@nextcloud/axios": "^2.0.0", "@nextcloud/axios": "^2.0.0",
"@nextcloud/dialogs": "^3.2.0",
"@nextcloud/initial-state": "^2.0.0", "@nextcloud/initial-state": "^2.0.0",
"@nextcloud/l10n": "^1.6.0",
"@nextcloud/logger": "^2.2.1", "@nextcloud/logger": "^2.2.1",
"@nextcloud/moment": "^1.2.1", "@nextcloud/moment": "^1.2.1",
"@nextcloud/router": "^1.2.0", "@nextcloud/router": "^1.2.0",

Wyświetl plik

@ -2,8 +2,11 @@
<NcContent v-if="!serverData.setup" app-name="social" :class="{public: serverData.public}"> <NcContent v-if="!serverData.setup" app-name="social" :class="{public: serverData.public}">
<NcAppNavigation v-if="!serverData.public"> <NcAppNavigation v-if="!serverData.public">
<template #list> <template #list>
<NcAppNavigationItem v-for="item in menu.items" :key="item.key" :to="item.to" <NcAppNavigationItem v-for="item in menu.items"
:title="item.title" :exact="true"> :key="item.key"
:to="item.to"
:title="item.title"
:exact="true">
<template #icon> <template #icon>
<component :is="item.icon" /> <component :is="item.icon" />
</template> </template>
@ -94,27 +97,27 @@ export default {
Search, Search,
}, },
mixins: [currentuserMixin], mixins: [currentuserMixin],
data: function() { data() {
return { return {
infoHidden: false, infoHidden: false,
state: [], state: [],
cloudAddress: '', cloudAddress: '',
searchTerm: '' searchTerm: '',
} }
}, },
computed: { computed: {
timeline: function() { timeline() {
return this.$store.getters.getTimeline return this.$store.getters.getTimeline
}, },
menu: function() { menu() {
const defaultCategories = [ const defaultCategories = [
{ {
id: 'social-timeline', id: 'social-timeline',
icon: Home, icon: Home,
title: t('social', 'Home'), title: t('social', 'Home'),
to: { to: {
name: 'timeline' name: 'timeline',
} },
}, },
{ {
id: 'social-direct-messages', id: 'social-direct-messages',
@ -122,8 +125,8 @@ export default {
title: t('social', 'Direct messages'), title: t('social', 'Direct messages'),
to: { to: {
name: 'timeline', name: 'timeline',
params: { type: 'direct' } params: { type: 'direct' },
} },
}, },
{ {
id: 'social-notifications', id: 'social-notifications',
@ -131,8 +134,8 @@ export default {
title: t('social', 'Notifications'), title: t('social', 'Notifications'),
to: { to: {
name: 'timeline', name: 'timeline',
params: { type: 'notifications' } params: { type: 'notifications' },
} },
}, },
{ {
id: 'social-account', id: 'social-account',
@ -140,8 +143,8 @@ export default {
title: t('social', 'Profile'), title: t('social', 'Profile'),
to: { to: {
name: 'profile', name: 'profile',
params: { account: this.currentUser.uid } params: { account: this.currentUser.uid },
} },
}, },
{ {
id: 'social-liked', id: 'social-liked',
@ -149,8 +152,8 @@ export default {
title: t('social', 'Liked'), title: t('social', 'Liked'),
to: { to: {
name: 'timeline', name: 'timeline',
params: { type: 'liked' } params: { type: 'liked' },
} },
}, },
{ {
id: 'social-local', id: 'social-local',
@ -158,8 +161,8 @@ export default {
title: t('social', 'Local timeline'), title: t('social', 'Local timeline'),
to: { to: {
name: 'timeline', name: 'timeline',
params: { type: 'timeline' } params: { type: 'timeline' },
} },
}, },
{ {
id: 'social-global', id: 'social-global',
@ -167,22 +170,22 @@ export default {
title: t('social', 'Global timeline'), title: t('social', 'Global timeline'),
to: { to: {
name: 'timeline', name: 'timeline',
params: { type: 'federated' } params: { type: 'federated' },
} },
} },
] ]
return { return {
items: defaultCategories, items: defaultCategories,
loading: false loading: false,
} }
} },
}, },
watch: { watch: {
$route(to, from) { $route(to, from) {
this.searchTerm = '' this.searchTerm = ''
} },
}, },
beforeMount: function() { beforeMount() {
// importing server data into the store // importing server data into the store
this.$store.commit('setServerData', loadState('social', 'serverData')) this.$store.commit('setServerData', loadState('social', 'serverData'))
@ -212,7 +215,7 @@ export default {
resetSearch() { resetSearch() {
this.searchTerm = '' this.searchTerm = ''
}, },
fromPushApp: function(data) { fromPushApp(data) {
// FIXME: might be better to use Timeline.type() ? // FIXME: might be better to use Timeline.type() ?
let timeline = 'home' let timeline = 'home'
if (this.$route.name === 'tags') { if (this.$route.name === 'tags') {
@ -227,8 +230,8 @@ export default {
if (data.source === 'timeline.direct' && timeline === 'direct') { if (data.source === 'timeline.direct' && timeline === 'direct') {
this.$store.dispatch('addToTimeline', [data.payload]) this.$store.dispatch('addToTimeline', [data.payload])
} }
} },
} },
} }
</script> </script>

Wyświetl plik

@ -21,9 +21,16 @@
--> -->
<template> <template>
<NcAvatar v-if="actor.local" :size="size" :user="actor.preferredUsername" <NcAvatar v-if="actor.local"
:display-name="actor.account" :disable-tooltip="true" :showUserStatus="false" /> :size="size"
<NcAvatar v-else :size="size" :url="avatarUrl" :showUserStatus="false" :user="actor.preferredUsername"
:display-name="actor.account"
:disable-tooltip="true"
:show-user-status="false" />
<NcAvatar v-else
:size="size"
:url="avatarUrl"
:show-user-status="false"
:disable-tooltip="true" /> :disable-tooltip="true" />
</template> </template>
@ -34,21 +41,21 @@ import { generateUrl } from '@nextcloud/router'
export default { export default {
name: 'ActorAvatar', name: 'ActorAvatar',
components: { components: {
NcAvatar NcAvatar,
}, },
props: { props: {
actor: { type: Object, default: () => {} }, actor: { type: Object, default: () => {} },
size: { type: Number, default: 32 } size: { type: Number, default: 32 },
}, },
data: function() { data() {
return { return {
followingText: t('social', 'Following') followingText: t('social', 'Following'),
} }
}, },
computed: { computed: {
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)
} },
} },
} }
</script> </script>

Wyświetl plik

@ -25,14 +25,16 @@
<div class="new-post" data-id=""> <div class="new-post" data-id="">
<input id="file-upload" <input id="file-upload"
ref="fileUploadInput" ref="fileUploadInput"
@change="handleFileChange($event)"
multiple multiple
type="file" type="file"
tabindex="-1" tabindex="-1"
aria-hidden="true" aria-hidden="true"
class="hidden-visually"> class="hidden-visually"
@change="handleFileChange($event)">
<div class="new-post-author"> <div class="new-post-author">
<NcAvatar :user="currentUser.uid" :display-name="currentUser.displayName" :disable-tooltip="true" <NcAvatar :user="currentUser.uid"
:display-name="currentUser.displayName"
:disable-tooltip="true"
:size="32" /> :size="32" />
<div class="post-author"> <div class="post-author">
<span class="post-author-name"> <span class="post-author-name">
@ -46,11 +48,12 @@
<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>
<actor-avatar :actor="replyTo.actor_info" :size="16" /> <ActorAvatar :actor="replyTo.actor_info" :size="16" />
<strong>{{ replyTo.actor_info.account }}</strong> <strong>{{ replyTo.actor_info.account }}</strong>
<NcButton type="tertiary" class="close-button" <NcButton type="tertiary"
@click="closeReply" class="close-button"
:aria-label="t('social', 'Close reply')"> :aria-label="t('social', 'Close reply')"
@click="closeReply">
<template #icon> <template #icon>
<Close :size="20" /> <Close :size="20" />
</template> </template>
@ -61,33 +64,42 @@
</div> </div>
</div> </div>
<form class="new-post-form" @submit.prevent="createPost"> <form class="new-post-form" @submit.prevent="createPost">
<vue-tribute :options="tributeOptions"> <VueTribute :options="tributeOptions">
<!-- eslint-disable-next-line vue/valid-v-model --> <!-- eslint-disable-next-line vue/valid-v-model -->
<div ref="composerInput" v-contenteditable:post.dangerousHTML="canType && !loading" class="message" <div ref="composerInput"
placeholder="What would you like to share?" :class="{'icon-loading': loading}" @keyup.prevent.enter="keyup" v-contenteditable:post.dangerousHTML="canType && !loading"
class="message"
placeholder="What would you like to share?"
:class="{'icon-loading': loading}"
@keyup.prevent.enter="keyup"
@tribute-replaced="updatePostFromTribute" /> @tribute-replaced="updatePostFromTribute" />
</vue-tribute> </VueTribute>
<PreviewGrid :uploading="false" :uploadProgress="0.4" :miniatures="previewUrls" /> <PreviewGrid :uploading="false"
:upload-progress="0.4"
:miniatures="previewUrls"
@deleted="deletePreview" />
<div class="options"> <div class="options">
<NcButton type="tertiary" <NcButton v-tooltip="t('social', 'Add attachment')"
@click.prevent="clickImportInput" type="tertiary"
:aria-label="t('social', 'Add attachment')" :aria-label="t('social', 'Add attachment')"
v-tooltip="t('social', 'Add attachment')"> @click.prevent="clickImportInput">
<template #icon> <template #icon>
<FileUpload :size="22" decorative title="" /> <FileUpload :size="22" decorative title="" />
</template> </template>
</NcButton> </NcButton>
<div class="new-post-form__emoji-picker"> <div class="new-post-form__emoji-picker">
<NcEmojiPicker ref="emojiPicker" :search="search" :close-on-select="false" <NcEmojiPicker ref="emojiPicker"
:search="search"
:close-on-select="false"
:container="container" :container="container"
@select="insert"> @select="insert">
<NcButton type="tertiary" <NcButton v-tooltip="t('social', 'Add emoji')"
type="tertiary"
:aria-haspopup="true" :aria-haspopup="true"
:aria-label="t('social', 'Add emoji')" :aria-label="t('social', 'Add emoji')">
v-tooltip="t('social', 'Add emoji')">
<template #icon> <template #icon>
<EmoticonOutline :size="22" decorative title="" /> <EmoticonOutline :size="22" decorative title="" />
</template> </template>
@ -96,17 +108,19 @@
</div> </div>
<div v-click-outside="hidePopoverMenu" class="popovermenu-parent"> <div v-click-outside="hidePopoverMenu" class="popovermenu-parent">
<NcButton type="tertiary" <NcButton v-tooltip="t('social', 'Visibility')"
:class="currentVisibilityIconClass" type="tertiary"
@click.prevent="togglePopoverMenu" :class="currentVisibilityIconClass"
v-tooltip="t('social', 'Visibility')" /> @click.prevent="togglePopoverMenu" />
<div :class="{open: menuOpened}" class="popovermenu"> <div :class="{open: menuOpened}" class="popovermenu">
<NcPopoverMenu :menu="visibilityPopover" /> <NcPopoverMenu :menu="visibilityPopover" />
</div> </div>
</div> </div>
<div class="emptySpace" /> <div class="emptySpace" />
<NcButton :value="currentVisibilityPostLabel" :disabled="!canPost" type="primary" <NcButton :value="currentVisibilityPostLabel"
:disabled="!canPost"
type="primary"
@click.prevent="createPost"> @click.prevent="createPost">
<template #icon> <template #icon>
<Send title="" :size="22" decorative /> <Send title="" :size="22" decorative />
@ -162,8 +176,8 @@ export default {
type: localStorage.getItem('social.lastPostType') || 'followers', type: localStorage.getItem('social.lastPostType') || 'followers',
loading: false, loading: false,
post: '', post: '',
miniatures: [], // miniatures of images stored in postAttachments miniatures: [], // miniatures of images stored in postAttachments
postAttachments: [], // The toot's attachments postAttachments: [], // The toot's attachments
previewUrls: [], previewUrls: [],
canType: true, canType: true,
search: '', search: '',
@ -173,45 +187,45 @@ export default {
collection: [ collection: [
{ {
trigger: '@', trigger: '@',
lookup: function(item) { lookup(item) {
return item.key + item.value return item.key + item.value
}, },
menuItemTemplate: function(item) { menuItemTemplate(item) {
return '<img src="' + item.original.avatar + '" /><div>' return '<img src="' + item.original.avatar + '" /><div>'
+ '<span class="displayName">' + item.original.key + '</span>' + '<span class="displayName">' + item.original.key + '</span>'
+ '<span class="account">' + item.original.value + '</span>' + '<span class="account">' + item.original.value + '</span>'
+ '</div>' + '</div>'
}, },
selectTemplate: function(item) { selectTemplate(item) {
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: (text, cb) => {
let users = [] const users = []
if (text.length < 1) { if (text.length < 1) {
cb(users) cb(users)
} }
this.remoteSearchAccounts(text).then((result) => { this.remoteSearchAccounts(text).then((result) => {
for (var i in result.data.result.accounts) { for (const i in result.data.result.accounts) {
let user = result.data.result.accounts[i] const user = result.data.result.accounts[i]
users.push({ users.push({
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) cb(users)
}) })
} },
}, },
{ {
trigger: '#', trigger: '#',
menuItemTemplate: function(item) { menuItemTemplate(item) {
return item.original.value return item.original.value
}, },
selectTemplate: function(item) { selectTemplate(item) {
let tag = '' let tag = ''
// item is undefined if selectTemplate is called from a noMatchTemplate menu // item is undefined if selectTemplate is called from a noMatchTemplate menu
if (typeof item === 'undefined') { if (typeof item === 'undefined') {
@ -223,7 +237,7 @@ export default {
+ '<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: (text, cb) => {
let tags = [] const tags = []
if (text.length < 1) { if (text.length < 1) {
cb(tags) cb(tags)
@ -232,20 +246,20 @@ export default {
if (result.data.result.exact) { if (result.data.result.exact) {
tags.push({ tags.push({
key: result.data.result.exact, key: result.data.result.exact,
value: result.data.result.exact value: result.data.result.exact,
}) })
} }
for (var i in result.data.result.tags) { for (const i in result.data.result.tags) {
let tag = result.data.result.tags[i] const tag = result.data.result.tags[i]
tags.push({ tags.push({
key: tag.hashtag, key: tag.hashtag,
value: tag.hashtag value: tag.hashtag,
}) })
} }
cb(tags) cb(tags)
}) })
} },
} },
], ],
noMatchTemplate() { noMatchTemplate() {
if (this.current.collection.trigger === '#') { if (this.current.collection.trigger === '#') {
@ -255,23 +269,24 @@ export default {
return '<li data-index="0">#' + this.current.mentionText + '</li>' return '<li data-index="0">#' + this.current.mentionText + '</li>'
} }
} }
} },
}, },
menuOpened: false menuOpened: false,
} }
}, },
computed: { computed: {
postTo() { postTo() {
switch (this.type) { switch (this.type) {
case 'public': case 'public':
case 'unlisted': case 'unlisted':
return t('social', 'Post') return t('social', 'Post')
case 'followers': case 'followers':
return t('social', 'Post to followers') return t('social', 'Post to followers')
case 'direct': case 'direct':
return t('social', 'Post to mentioned users') return t('social', 'Post to mentioned users')
} }
return ''
}, },
currentVisibilityIconClass() { currentVisibilityIconClass() {
return this.visibilityIconClass(this.type) return this.visibilityIconClass(this.type)
@ -331,7 +346,7 @@ export default {
icon: this.visibilityIconClass('public'), icon: this.visibilityIconClass('public'),
active: this.activeState('public'), active: this.activeState('public'),
text: t('social', 'Public'), text: t('social', 'Public'),
longtext: t('social', 'Post to public timelines') longtext: t('social', 'Post to public timelines'),
}, },
{ {
action: () => { action: () => {
@ -340,7 +355,7 @@ export default {
icon: this.visibilityIconClass('unlisted'), icon: this.visibilityIconClass('unlisted'),
active: this.activeState('unlisted'), active: this.activeState('unlisted'),
text: t('social', 'Unlisted'), text: t('social', 'Unlisted'),
longtext: t('social', 'Do not post to public timelines') longtext: t('social', 'Do not post to public timelines'),
}, },
{ {
action: () => { action: () => {
@ -349,7 +364,7 @@ export default {
icon: this.visibilityIconClass('followers'), icon: this.visibilityIconClass('followers'),
active: this.activeState('followers'), active: this.activeState('followers'),
text: t('social', 'Followers'), text: t('social', 'Followers'),
longtext: t('social', 'Post to followers only') longtext: t('social', 'Post to followers only'),
}, },
{ {
action: () => { action: () => {
@ -358,8 +373,8 @@ export default {
icon: this.visibilityIconClass('direct'), icon: this.visibilityIconClass('direct'),
active: this.activeState('direct'), active: this.activeState('direct'),
text: t('social', 'Direct'), text: t('social', 'Direct'),
longtext: t('social', 'Post to mentioned users only') longtext: t('social', 'Post to mentioned users only'),
} },
] ]
}, },
container() { container() {
@ -370,10 +385,10 @@ export default {
}, },
canPost() { canPost() {
if (this.previewUrls.length > 0) { if (this.previewUrls.length > 0) {
return true; return true
} }
return this.post.length !== 0 && this.post !== '<br>' return this.post.length !== 0 && this.post !== '<br>'
} },
}, },
mounted() { mounted() {
this.$root.$on('composer-reply', (data) => { this.$root.$on('composer-reply', (data) => {
@ -398,9 +413,9 @@ export default {
}, },
insert(emoji) { insert(emoji) {
if (typeof emoji === 'object') { if (typeof emoji === 'object') {
let category = Object.keys(emoji)[0] const category = Object.keys(emoji)[0]
let emojis = emoji[category] const emojis = emoji[category]
let firstEmoji = Object.keys(emojis)[0] const firstEmoji = Object.keys(emojis)[0]
emoji = emojis[firstEmoji] emoji = emojis[firstEmoji]
} }
this.post += this.$twemoji.parse(emoji) + ' ' this.post += this.$twemoji.parse(emoji) + ' '
@ -419,16 +434,16 @@ export default {
localStorage.setItem('social.lastPostType', type) localStorage.setItem('social.lastPostType', type)
}, },
getPostData() { getPostData() {
let element = this.$refs.composerInput.cloneNode(true) const element = this.$refs.composerInput.cloneNode(true)
Array.from(element.getElementsByClassName('emoji')).forEach((emoji) => { Array.from(element.getElementsByClassName('emoji')).forEach((emoji) => {
var em = document.createTextNode(emoji.getAttribute('alt')) const em = document.createTextNode(emoji.getAttribute('alt'))
emoji.replaceWith(em) emoji.replaceWith(em)
}) })
let contentHtml = element.innerHTML const contentHtml = element.innerHTML
// Extract mentions from content and create an array out of them // Extract mentions from content and create an array out of them
let to = [] const to = []
const mentionRegex = /<span class="mention"[^>]+><a[^>]+><img[^>]+>@([\w-_.]+@[\w-.]+)/g const mentionRegex = /<span class="mention"[^>]+><a[^>]+><img[^>]+>@([\w-_.]+@[\w-.]+)/g
let match = null let match = null
do { do {
@ -445,7 +460,7 @@ export default {
// Extract hashtags from content and create an array ot of them // Extract hashtags from content and create an array ot of them
const hashtagRegex = />#([^<]+)</g const hashtagRegex = />#([^<]+)</g
let hashtags = [] const hashtags = []
match = null match = null
do { do {
match = hashtagRegex.exec(contentHtml) match = hashtagRegex.exec(contentHtml)
@ -458,7 +473,7 @@ export default {
let content = contentHtml.replace(/<(?!\/div)[^>]+>/gi, '').replace(/<\/div>/gi, '\n').trim() let content = contentHtml.replace(/<(?!\/div)[^>]+>/gi, '').replace(/<\/div>/gi, '\n').trim()
content = he.decode(content) content = he.decode(content)
let formData = new FormData() const formData = new FormData()
formData.append('content', content) formData.append('content', content)
formData.append('to', to) formData.append('to', to)
formData.append('hashtags', hashtags) formData.append('hashtags', hashtags)
@ -483,17 +498,17 @@ export default {
// Trick to let vue-contenteditable know that tribute replaced a mention or hashtag // Trick to let vue-contenteditable know that tribute replaced a mention or hashtag
this.$refs.composerInput.oninput(event) this.$refs.composerInput.oninput(event)
}, },
createPost: async function(event) { async createPost(event) {
let postData = this.getPostData() const postData = this.getPostData()
// Trick to validate last mention when the user directly clicks on the "post" button without validating it. // Trick to validate last mention when the user directly clicks on the "post" button without validating it.
let regex = /@([-\w]+)$/ const regex = /@([-\w]+)$/
let lastMention = postData.get('content').match(regex) const lastMention = postData.get('content').match(regex)
if (lastMention) { if (lastMention) {
// Ask the server for matching accounts, and wait for the results // Ask the server for matching accounts, and wait for the results
let result = await this.remoteSearchAccounts(lastMention[1]) const result = await this.remoteSearchAccounts(lastMention[1])
// Validate the last mention only when it matches a single account // Validate the last mention only when it matches a single account
if (result.data.result.accounts.length === 1) { if (result.data.result.accounts.length === 1) {
@ -504,8 +519,8 @@ export default {
// Abort if the post is a direct message and no valid mentions were found // Abort if the post is a direct message and no valid mentions were found
// if (this.type === 'direct' && postData.get('to').length === 0) { // if (this.type === 'direct' && postData.get('to').length === 0) {
// OC.Notification.showTemporary(t('social', 'Error while trying to post your message: Could not find any valid recipients.'), { type: 'error' }) // OC.Notification.showTemporary(t('social', 'Error while trying to post your message: Could not find any valid recipients.'), { type: 'error' })
// return // return
// } // }
// Post message // Post message
@ -530,8 +545,11 @@ export default {
}, },
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?search=' + text))
} },
} deletePreview(index) {
this.previewUrls.splice(index, 1)
},
},
} }
</script> </script>

Wyświetl plik

@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-or-later
<template> <template>
<div class="upload-form"> <div class="upload-form">
<div class="upload-progress" v-if="false"> <div v-if="false" class="upload-progress">
<div class="upload-progress__icon"> <div class="upload-progress__icon">
<FileUpload :size="32" /> <FileUpload :size="32" />
</div> </div>
@ -19,14 +19,18 @@ 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" :key="index" :preview="item" :index="index" @delete="deletePreview" /> <PreviewGridItem v-for="(item, index) in miniatures"
:key="index"
:preview="item"
:index="index"
@delete="deletePreview" />
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import PreviewGridItem from './PreviewGridItem' import PreviewGridItem from './PreviewGridItem.vue'
import FileUpload from 'vue-material-design-icons/FileUpload' import FileUpload from 'vue-material-design-icons/FileUpload.vue'
export default { export default {
name: 'PreviewGrid', name: 'PreviewGrid',
@ -50,9 +54,8 @@ export default {
}, },
methods: { methods: {
deletePreview(index) { deletePreview(index) {
console.debug("rjeoijreo") $emit('deleted', index)
this.miniatures.splice(index, 1) },
}
}, },
} }
</script> </script>

Wyświetl plik

@ -4,62 +4,54 @@
<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', index)">
<template #icon> <template #icon>
<Close :size="16" fillColor="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"> <NcButton type="tertiary-no-background" @click="showModal">
<template #icon> <template #icon>
<Edit :size="16" fillColor="white" /> <Edit :size="16" fill-color="white" />
</template> </template>
<span>{{ t('social', 'Edit') }}</span> <span>{{ t('social', 'Edit') }}</span>
</NcButton> </NcButton>
-->
</div> </div>
<div class="description-warning" v-if="preview.description.length === 0"> <!--
<div v-if="preview.description.length === 0" class="description-warning">
{{ t('social', 'No description added') }} {{ t('social', 'No description added') }}
</div> </div>
<NcModal v-if="modal" @close="closeModal" size="small"> <NcModal v-if="modal" size="small" @close="closeModal">
<div class="modal__content"> <div class="modal__content">
<label :for="`image-description-${index}`"> <label :for="`image-description-${index}`">
{{ t('social', 'Describe for the visually impaired') }} {{ t('social', 'Describe for the visually impaired') }}
</label> </label>
<textarea :id="`image-description-${index}`" v-model="preview.description"> <textarea :id="`image-description-${index}`" v-model="preview.description" />
</textarea> <NcButton type="primary" @click="closeModal">
<NcButton type="primary" @click="closeModal">{{ t('social', 'Close') }}</NcButton> {{ t('social', 'Close') }}
</NcButton>
</div> </div>
</NcModal> </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 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 NcModal from '@nextcloud/vue/dist/Components/NcModal.js'
export default { export default {
name: 'PreviewGridItem', name: 'PreviewGridItem',
components: { components: {
Close, Close,
Edit, // Edit,
// NcModal,
NcButton, NcButton,
NcModal,
},
data() {
return {
modal: false,
}
},
methods: {
showModal() {
this.modal = true
},
closeModal() {
this.modal = false
}
}, },
props: { props: {
preview: { preview: {
@ -71,6 +63,11 @@ export default {
required: true, required: true,
}, },
}, },
data() {
return {
modal: false,
}
},
computed: { computed: {
backgroundStyle() { backgroundStyle() {
return { return {
@ -78,6 +75,14 @@ export default {
} }
}, },
}, },
methods: {
showModal() {
this.modal = true
},
closeModal() {
this.modal = false
},
},
} }
</script> </script>

Wyświetl plik

@ -1,5 +1,7 @@
<template> <template>
<img class="emoji" draggable="false" :alt="emoji" <img class="emoji"
draggable="false"
:alt="emoji"
:src="emojiUrl"> :src="emojiUrl">
</template> </template>
@ -14,9 +16,9 @@ const UFE0Fg = /\uFE0F/g
export default { export default {
name: 'Emoji', name: 'Emoji',
props: { props: {
emoji: { type: String, default: '' } emoji: { type: String, default: '' },
}, },
data: function() { data() {
return {} return {}
}, },
computed: { computed: {
@ -28,8 +30,8 @@ export default {
}, },
emojiUrl() { emojiUrl() {
return generateFilePath('social', 'img', 'twemoji/' + this.icon + '.svg') return generateFilePath('social', 'img', 'twemoji/' + this.icon + '.svg')
} },
} },
} }
</script> </script>
<style scoped> <style scoped>

Wyświetl plik

@ -36,13 +36,13 @@
export default { export default {
name: 'EmptyContent', name: 'EmptyContent',
props: { props: {
item: { type: Object, default: () => {} } item: { type: Object, default: () => {} },
}, },
computed: { computed: {
imageUrl() { imageUrl() {
return OC.linkTo('social', this.item.image) return OC.linkTo('social', this.item.image)
} },
} },
} }
</script> </script>
<style scoped> <style scoped>

Wyświetl plik

@ -23,12 +23,16 @@
<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 && accountInfo && accountInfo.viewerLink!='viewer'">
<button v-if="isCurrentUserFollowing" :class="{'icon-loading-small': followLoading}" <button v-if="isCurrentUserFollowing"
:class="{'icon-loading-small': followLoading}"
@click="unfollow()" @click="unfollow()"
@mouseover="followingText=t('social', 'Unfollow')" @mouseleave="followingText=t('social', 'Following')"> @mouseover="followingText=t('social', 'Unfollow')"
@mouseleave="followingText=t('social', 'Following')">
<span><span class="icon-checkmark" />{{ followingText }}</span> <span><span class="icon-checkmark" />{{ followingText }}</span>
</button> </button>
<button v-else :class="{'icon-loading-small': followLoading}" class="primary" <button v-else
:class="{'icon-loading-small': followLoading}"
class="primary"
@click="follow"> @click="follow">
<span>{{ t('social', 'Follow') }}</span> <span>{{ t('social', 'Follow') }}</span>
</button> </button>
@ -43,21 +47,21 @@ export default {
name: 'FollowButton', name: 'FollowButton',
mixins: [ mixins: [
accountMixins, accountMixins,
currentUser currentUser,
], ],
props: { props: {
account: { account: {
type: String, type: String,
default: '' default: '',
}, },
uid: { uid: {
type: String, type: String,
default: '' default: '',
} },
}, },
data: function() { data() {
return { return {
followingText: t('social', 'Following') followingText: t('social', 'Following'),
} }
}, },
computed: { computed: {
@ -66,7 +70,7 @@ export default {
}, },
isCurrentUserFollowing() { isCurrentUserFollowing() {
return this.$store.getters.isFollowingUser(this.account) return this.$store.getters.isFollowingUser(this.account)
} },
}, },
methods: { methods: {
follow() { follow() {
@ -74,8 +78,8 @@ export default {
}, },
unfollow() { unfollow() {
this.$store.dispatch('unfollowAccount', { currentAccount: this.cloudId, accountToUnfollow: this.account }) this.$store.dispatch('unfollowAccount', { currentAccount: this.cloudId, accountToUnfollow: this.account })
} },
} },
} }
</script> </script>
<style scoped> <style scoped>

File diff suppressed because one or more lines are too long

Wyświetl plik

@ -3,8 +3,12 @@
<div v-for="(item, index) in attachments" :key="index"> <div v-for="(item, index) in attachments" :key="index">
<img :src="imageUrl(item)" @click="showModal(index)"> <img :src="imageUrl(item)" @click="showModal(index)">
</div> </div>
<NcModal v-show="modal" :has-previous="current > 0" :has-next="current < (attachments.length - 1)" <NcModal v-show="modal"
size="full" @close="closeModal" @previous="showPrevious" :has-previous="current > 0"
:has-next="current < (attachments.length - 1)"
size="full"
@close="closeModal"
@previous="showPrevious"
@next="showNext"> @next="showNext">
<div class="modal__content"> <div class="modal__content">
<canvas ref="modalCanvas" /> <canvas ref="modalCanvas" />
@ -22,21 +26,21 @@ import { generateUrl } from '@nextcloud/router'
export default { export default {
name: 'PostAttachment', name: 'PostAttachment',
components: { components: {
NcModal NcModal,
}, },
mixins: [ mixins: [
serverData serverData,
], ],
props: { props: {
attachments: { attachments: {
type: Array, type: Array,
default: Array default: Array,
} },
}, },
data() { data() {
return { return {
modal: false, modal: false,
current: '' current: '',
} }
}, },
methods: { methods: {
@ -44,7 +48,7 @@ export default {
* @function imageUrl * @function imageUrl
* @description Returns the URL where to get a resized version of the attachement * @description Returns the URL where to get a resized version of the attachement
* @param {object} item - The attachment * @param {object} item - The attachment
* @returns {string} The URL * @return {string} The URL
*/ */
imageUrl(item) { imageUrl(item) {
if (this.serverData.public) { if (this.serverData.public) {
@ -58,12 +62,12 @@ export default {
* @description Displays the currently selected attachment's image * @description Displays the currently selected attachment's image
*/ */
displayImage() { displayImage() {
var canvas = this.$refs.modalCanvas const canvas = this.$refs.modalCanvas
var ctx = canvas.getContext('2d') const ctx = canvas.getContext('2d')
var img = new Image() const img = new Image()
img.onload = function() { img.onload = function() {
var width = img.width let width = img.width
var height = img.height let height = img.height
if (width > window.innerWidth) { if (width > window.innerWidth) {
height = height * (window.innerWidth / width) height = height * (window.innerWidth / width)
width = window.innerWidth width = window.innerWidth
@ -93,7 +97,7 @@ export default {
showNext() { showNext() {
this.current++ this.current++
this.displayImage() this.displayImage()
} },
} },
} }
</script> </script>

Wyświetl plik

@ -22,9 +22,13 @@
<template> <template>
<div v-if="profileAccount && accountInfo" class="user-profile"> <div v-if="profileAccount && accountInfo" class="user-profile">
<NcAvatar v-if="accountInfo.local" :user="localUid" :disable-tooltip="true" <NcAvatar v-if="accountInfo.local"
:user="localUid"
:disable-tooltip="true"
:size="128" /> :size="128" />
<NcAvatar v-else :url="avatarUrl" :disable-tooltip="true" <NcAvatar v-else
:url="avatarUrl"
:disable-tooltip="true"
: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 -->
@ -51,13 +55,77 @@
{{ accountInfo.website.value }} {{ accountInfo.website.value }}
</a> </a>
</p> </p>
<follow-button :account="accountInfo.account" :uid="uid" /> <FollowButton :account="accountInfo.account" :uid="uid" />
<NcButton v-if="serverData.public" class="primary" @click="followRemote"> <NcButton v-if="serverData.public" class="primary" @click="followRemote">
{{ t('social', 'Follow') }} {{ t('social', 'Follow') }}
</NcButton> </NcButton>
</div> </div>
</template> </template>
<script>
import NcAvatar from '@nextcloud/vue/dist/Components/NcAvatar.js'
import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
import accountMixins from '../mixins/accountMixins.js'
import serverData from '../mixins/serverData.js'
import currentUser from '../mixins/currentUserMixin.js'
import follow from '../mixins/follow.js'
import FollowButton from './FollowButton.vue'
import { generateUrl } from '@nextcloud/router'
export default {
name: 'ProfileInfo',
components: {
FollowButton,
NcAvatar,
NcButton,
},
mixins: [
accountMixins,
currentUser,
serverData,
follow,
],
props: {
uid: {
type: String,
default: '',
},
},
data() {
return {
followingText: t('social', 'Following'),
}
},
computed: {
localUid() {
// Returns only the local part of a username
return (this.uid.indexOf('@') === -1) ? this.uid : this.uid.slice(0, this.uid.indexOf('@'))
},
displayName() {
if (typeof this.accountInfo.name !== 'undefined' && this.accountInfo.name !== '') {
return this.accountInfo.name
}
if (typeof this.accountInfo.preferredUsername !== 'undefined' && this.accountInfo.preferredUsername !== '') {
return this.accountInfo.preferredUsername
}
return this.profileAccount
},
getCount() {
const account = this.accountInfo
return (field) => account.details.count ? account.details.count[field] : ''
},
avatarUrl() {
return generateUrl('/apps/social/api/v1/global/actor/avatar?id=' + this.accountInfo.id)
},
},
methods: {
followRemote() {
window.open(generateUrl('/apps/social/api/v1/ostatus/followRemote/' + encodeURI(this.localUid)), 'followRemote', 'width=433,height=600toolbar=no,menubar=no,scrollbars=yes,resizable=yes')
},
},
}
</script>
<style scoped> <style scoped>
.user-profile { .user-profile {
display: flex; display: flex;
@ -94,67 +162,3 @@
border-bottom: 1px solid var(--color-main-text); border-bottom: 1px solid var(--color-main-text);
} }
</style> </style>
<script>
import NcAvatar from '@nextcloud/vue/dist/Components/NcAvatar.js'
import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
import accountMixins from '../mixins/accountMixins.js'
import serverData from '../mixins/serverData.js'
import currentUser from '../mixins/currentUserMixin.js'
import follow from '../mixins/follow.js'
import FollowButton from './FollowButton.vue'
import { generateUrl } from '@nextcloud/router'
export default {
name: 'ProfileInfo',
components: {
FollowButton,
NcAvatar,
NcButton,
},
mixins: [
accountMixins,
currentUser,
serverData,
follow
],
props: {
uid: {
type: String,
default: ''
}
},
data: function() {
return {
followingText: t('social', 'Following')
}
},
computed: {
localUid() {
// Returns only the local part of a username
return (this.uid.indexOf('@') === -1) ? this.uid : this.uid.slice(0, this.uid.indexOf('@'))
},
displayName() {
if (typeof this.accountInfo.name !== 'undefined' && this.accountInfo.name !== '') {
return this.accountInfo.name
}
if (typeof this.accountInfo.preferredUsername !== 'undefined' && this.accountInfo.preferredUsername !== '') {
return this.accountInfo.preferredUsername
}
return this.profileAccount
},
getCount() {
let account = this.accountInfo
return (field) => account.details.count ? account.details.count[field] : ''
},
avatarUrl() {
return generateUrl('/apps/social/api/v1/global/actor/avatar?id=' + this.accountInfo.id)
}
},
methods: {
followRemote() {
window.open(generateUrl('/apps/social/api/v1/ostatus/followRemote/' + encodeURI(this.localUid)), 'followRemote', 'width=433,height=600toolbar=no,menubar=no,scrollbars=yes,resizable=yes')
}
}
}
</script>

Wyświetl plik

@ -33,15 +33,17 @@
</div> </div>
<div v-else> <div v-else>
<h3>{{ t('social', 'Searching for') }} {{ decodeURIComponent(term) }}</h3> <h3>{{ t('social', 'Searching for') }} {{ decodeURIComponent(term) }}</h3>
<user-entry v-for="result in allResults" :key="result.id" :item="result" /> <UserEntry v-for="result in allResults" :key="result.id" :item="result" />
<div v-if="hashtags.length > 0"> <div v-if="hashtags.length > 0">
<li v-for="tag in hashtags" :key="tag.hashtag" class="tag"> <li v-for="tag in hashtags" :key="tag.hashtag" class="tag">
<router-link :to="{ name: 'tags', params: {tag: tag.hashtag } }"> <router-link :to="{ name: 'tags', params: {tag: tag.hashtag } }">
<span>#{{ tag.hashtag }}</span> <span>#{{ tag.hashtag }}</span>
<trend <Trend :data="trendData(tag.trend)"
:data="trendData(tag.trend)" :gradient="['#17adff', '#0082c9']"
:gradient="['#17adff', '#0082c9']" :smooth="true" :width="150" :smooth="true"
:height="44" stroke-width="2" /> :width="150"
:height="44"
stroke-width="2" />
</router-link> </router-link>
</li> </li>
</div> </div>
@ -49,33 +51,6 @@
</div> </div>
</template> </template>
<style scoped lang="scss">
.user-entry {
padding: 0;
}
h3 {
margin-top: -3px;
margin-left: 47px;
}
.tag {
list-style-type: none;
margin: 0;
padding: 0;
border-bottom: 1px solid var(--color-background-dark);
a {
display: flex;
span {
display: inline-block;
padding: 12px;
font-weight: 300;
flex-grow: 1;
}
}
}
</style>
<script> <script>
import UserEntry from './UserEntry.vue' import UserEntry from './UserEntry.vue'
@ -87,13 +62,13 @@ export default {
name: 'Search', name: 'Search',
components: { components: {
UserEntry, UserEntry,
Trend Trend,
}, },
props: { props: {
term: { term: {
type: String, type: String,
default: '' default: '',
} },
}, },
data() { data() {
return { return {
@ -101,7 +76,7 @@ export default {
loading: false, loading: false,
remoteLoading: false, remoteLoading: false,
match: null, match: null,
hashtags: [] hashtags: [],
} }
}, },
computed: { computed: {
@ -113,12 +88,12 @@ export default {
return this.results.accounts.result return this.results.accounts.result
} }
return [] return []
} },
}, },
watch: { watch: {
term(val) { term(val) {
this.search(val) this.search(val)
} },
}, },
beforeMount() { beforeMount() {
this.search(this.term) this.search(this.term)
@ -130,7 +105,7 @@ export default {
Math.max(0, trend['3d'] - trend['1d']), Math.max(0, trend['3d'] - trend['1d']),
Math.max(0, trend['1d'] - trend['12h']), Math.max(0, trend['1d'] - trend['12h']),
Math.max(0, trend['12h'] - trend['1h']), Math.max(0, trend['12h'] - trend['1h']),
Math.max(0, trend['1h']) Math.max(0, trend['1h']),
] ]
return data return data
}, },
@ -162,7 +137,34 @@ 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))
} },
} },
} }
</script> </script>
<style scoped lang="scss">
.user-entry {
padding: 0;
}
h3 {
margin-top: -3px;
margin-left: 47px;
}
.tag {
list-style-type: none;
margin: 0;
padding: 0;
border-bottom: 1px solid var(--color-background-dark);
a {
display: flex;
span {
display: inline-block;
padding: 12px;
font-weight: 300;
flex-grow: 1;
}
}
}
</style>

Wyświetl plik

@ -24,12 +24,11 @@
{{ boosted }} {{ boosted }}
</div> </div>
</template> </template>
<user-entry v-if="item.type === 'SocialAppNotification' && item.details.actor" :key="item.details.actor.id" :item="item.details.actor" /> <UserEntry v-if="item.type === 'SocialAppNotification' && item.details.actor" :key="item.details.actor.id" :item="item.details.actor" />
<template v-else> <template v-else>
<div class="wrapper"> <div class="wrapper">
<timeline-avatar :item="entryContent" /> <TimelineAvatar :item="entryContent" />
<timeline-post <TimelinePost class="message"
class="message"
:item="entryContent" :item="entryContent"
:parent-announce="isBoost" /> :parent-announce="isBoost" />
</div> </div>
@ -47,12 +46,12 @@ export default {
components: { components: {
TimelinePost, TimelinePost,
TimelineAvatar, TimelineAvatar,
UserEntry UserEntry,
}, },
props: { props: {
item: { item: {
type: Object, type: Object,
default: () => {} default: () => {},
}, },
isProfilePage: { isProfilePage: {
type: Boolean, type: Boolean,
@ -88,13 +87,13 @@ export default {
actionSummary() { actionSummary() {
let summary = this.item.summary let summary = this.item.summary
for (var key in this.item.details) { for (const key in this.item.details) {
let keyword = '{' + key + '}' const keyword = '{' + key + '}'
if (typeof this.item.details[key] !== 'string' && this.item.details[key].length > 1) { if (typeof this.item.details[key] !== 'string' && this.item.details[key].length > 1) {
let concatination = '' let concatination = ''
for (var stringKey in this.item.details[key]) { for (const stringKey in this.item.details[key]) {
if (this.item.details[key].length > 3 && stringKey === '3') { if (this.item.details[key].length > 3 && stringKey === '3') {
// ellipses the actors' list to 3 actors when it's big // ellipses the actors' list to 3 actors when it's big
@ -115,13 +114,13 @@ export default {
} }
return summary return summary
} },
}, },
methods: { methods: {
userDisplayName(actorInfo) { userDisplayName(actorInfo) {
return actorInfo.name !== '' ? actorInfo.name : actorInfo.preferredUsername return actorInfo.name !== '' ? actorInfo.name : actorInfo.preferredUsername
} },
} },
} }
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">

Wyświetl plik

@ -23,9 +23,9 @@
<template> <template>
<div class="social__timeline"> <div class="social__timeline">
<transition-group name="list" tag="div"> <transition-group name="list" tag="div">
<timeline-entry v-for="entry in timeline" :key="entry.id" :item="entry" /> <TimelineEntry v-for="entry in timeline" :key="entry.id" :item="entry" />
</transition-group> </transition-group>
<infinite-loading ref="infiniteLoading" @infinite="infiniteHandler"> <InfiniteLoading ref="infiniteLoading" @infinite="infiniteHandler">
<div slot="spinner"> <div slot="spinner">
<div class="icon-loading" /> <div class="icon-loading" />
</div> </div>
@ -33,12 +33,126 @@
<div class="list-end" /> <div class="list-end" />
</div> </div>
<div slot="no-results"> <div slot="no-results">
<empty-content v-if="timeline.length === 0" :item="emptyContentData" /> <EmptyContent v-if="timeline.length === 0" :item="emptyContentData" />
</div> </div>
</infinite-loading> </InfiniteLoading>
</div> </div>
</template> </template>
<script>
import InfiniteLoading from 'vue-infinite-loading'
import TimelineEntry from './TimelineEntry.vue'
import CurrentUserMixin from './../mixins/currentUserMixin.js'
import EmptyContent from './EmptyContent.vue'
import Logger from '../logger.js'
export default {
name: 'TimelineList',
components: {
TimelineEntry,
InfiniteLoading,
EmptyContent,
},
mixins: [CurrentUserMixin],
props: {
type: { type: String, default: () => 'home' },
},
data() {
return {
infoHidden: false,
state: [],
emptyContent: {
default: {
image: 'img/undraw/posts.svg',
title: t('social', 'No posts found'),
description: t('social', 'Posts from people you follow will show up here'),
},
direct: {
image: 'img/undraw/direct.svg',
title: t('social', 'No direct messages found'),
description: t('social', 'Posts directed to you will show up here'),
},
timeline: {
image: 'img/undraw/local.svg',
title: t('social', 'No local posts found'),
description: t('social', 'Posts from other people on this instance will show up here'),
},
notifications: {
image: 'img/undraw/notifications.svg',
title: t('social', 'No notifications found'),
description: t('social', 'You have not received any notifications yet'),
},
federated: {
image: 'img/undraw/global.svg',
title: t('social', 'No global posts found'),
description: t('social', 'Posts from federated instances will show up here'),
},
liked: {
image: 'img/undraw/likes.svg',
title: t('social', 'No liked posts found'),
},
profile: {
image: 'img/undraw/profile.svg',
title: t('social', 'You have not tooted yet'),
},
tags: {
image: 'img/undraw/profile.svg',
title: t('social', 'No posts found for this tag'),
},
'single-post': {
title: t('social', 'No replies found'),
},
},
}
},
computed: {
emptyContentData() {
if (typeof this.emptyContent[this.$route.params.type] !== 'undefined') {
return this.emptyContent[this.$route.params.type]
}
if (typeof this.emptyContent[this.$route.name] !== 'undefined') {
const content = this.emptyContent[this.$route.name]
// Change text on profile page when accessed by another user or a public (non-authenticated) user
if (this.$route.name === 'profile' && (this.serverData.public || this.$route.params.account !== this.currentUser.uid)) {
content.title = this.$route.params.account + ' ' + t('social', 'hasn\'t tooted yet')
}
return this.$route.name === 'timeline' ? this.emptyContent.default : content
}
// Fallback
Logger.log('Did not find any empty content for this route', { routeType: this.$route.params.type, routeName: this.$route.name })
return this.emptyContent.default
},
timeline() {
return this.$store.getters.getTimeline
},
},
beforeMount() {
},
methods: {
infiniteHandler($state) {
this.$store.dispatch('fetchTimeline', {
account: this.currentUser.uid,
}).then((response) => {
if (response.status === -1) {
OC.Notification.showTemporary('Failed to load more timeline entries')
console.error('Failed to load more timeline entries', response)
$state.complete()
return
}
response.result.length > 0 ? $state.loaded() : $state.complete()
}).catch((error) => {
OC.Notification.showTemporary('Failed to load more timeline entries')
console.error('Failed to load more timeline entries', error)
$state.complete()
})
},
},
}
</script>
<style scoped> <style scoped>
.list-item { .list-item {
} }
@ -54,117 +168,3 @@
transform: translateX(-100px); transform: translateX(-100px);
} }
</style> </style>
<script>
import InfiniteLoading from 'vue-infinite-loading'
import TimelineEntry from './TimelineEntry.vue'
import CurrentUserMixin from './../mixins/currentUserMixin.js'
import EmptyContent from './EmptyContent.vue'
import Logger from '../logger.js'
export default {
name: 'Timeline',
components: {
TimelineEntry,
InfiniteLoading,
EmptyContent
},
mixins: [CurrentUserMixin],
props: {
type: { type: String, default: () => 'home' }
},
data() {
return {
infoHidden: false,
state: [],
emptyContent: {
default: {
image: 'img/undraw/posts.svg',
title: t('social', 'No posts found'),
description: t('social', 'Posts from people you follow will show up here')
},
direct: {
image: 'img/undraw/direct.svg',
title: t('social', 'No direct messages found'),
description: t('social', 'Posts directed to you will show up here')
},
timeline: {
image: 'img/undraw/local.svg',
title: t('social', 'No local posts found'),
description: t('social', 'Posts from other people on this instance will show up here')
},
notifications: {
image: 'img/undraw/notifications.svg',
title: t('social', 'No notifications found'),
description: t('social', 'You have not received any notifications yet')
},
federated: {
image: 'img/undraw/global.svg',
title: t('social', 'No global posts found'),
description: t('social', 'Posts from federated instances will show up here')
},
liked: {
image: 'img/undraw/likes.svg',
title: t('social', 'No liked posts found')
},
profile: {
image: 'img/undraw/profile.svg',
title: t('social', 'You have not tooted yet')
},
tags: {
image: 'img/undraw/profile.svg',
title: t('social', 'No posts found for this tag')
},
'single-post': {
title: t('social', 'No replies found')
}
}
}
},
computed: {
emptyContentData() {
if (typeof this.emptyContent[this.$route.params.type] !== 'undefined') {
return this.emptyContent[this.$route.params.type]
}
if (typeof this.emptyContent[this.$route.name] !== 'undefined') {
let content = this.emptyContent[this.$route.name]
// Change text on profile page when accessed by another user or a public (non-authenticated) user
if (this.$route.name === 'profile' && (this.serverData.public || this.$route.params.account !== this.currentUser.uid)) {
content.title = this.$route.params.account + ' ' + t('social', 'hasn\'t tooted yet')
}
return this.$route.name === 'timeline' ? this.emptyContent['default'] : content
}
// Fallback
Logger.log('Did not find any empty content for this route', { 'routeType': this.$route.params.type, 'routeName': this.$route.name })
return this.emptyContent.default
},
timeline: function() {
return this.$store.getters.getTimeline
}
},
beforeMount: function() {
},
methods: {
infiniteHandler($state) {
this.$store.dispatch('fetchTimeline', {
account: this.currentUser.uid
}).then((response) => {
if (response.status === -1) {
OC.Notification.showTemporary('Failed to load more timeline entries')
console.error('Failed to load more timeline entries', response)
$state.complete()
return
}
response.result.length > 0 ? $state.loaded() : $state.complete()
}).catch((error) => {
OC.Notification.showTemporary('Failed to load more timeline entries')
console.error('Failed to load more timeline entries', error)
$state.complete()
})
}
}
}
</script>

Wyświetl plik

@ -33,34 +33,34 @@
<!-- 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.actor_info.summary" />
<div v-if="hasAttachments" class="post-attachments"> <div v-if="hasAttachments" class="post-attachments">
<post-attachment :attachments="item.attachment" /> <PostAttachment :attachments="item.attachment" />
</div> </div>
<div v-if="this.$route && this.$route.params.type !== 'notifications' && !serverData.public" class="post-actions"> <div v-if="$route && $route.params.type !== 'notifications' && !serverData.public" class="post-actions">
<NcButton type="tertiary-no-background" <NcButton v-tooltip="t('social', 'Reply')"
v-tooltip="t('social', 'Reply')" type="tertiary-no-background"
@click="reply"> @click="reply">
<template #icon> <template #icon>
<Reply :size="20" /> <Reply :size="20" />
</template> </template>
</NcButton> </NcButton>
<NcButton type="tertiary-no-background" <NcButton v-tooltip="t('social', 'Boost')"
v-tooltip="t('social', 'Boost')" type="tertiary-no-background"
@click="boost"> @click="boost">
<template #icon> <template #icon>
<Repeat :size="20" :fill-color="isBoosted ? 'blue' : 'var(--color-main-text)'" /> <Repeat :size="20" :fill-color="isBoosted ? 'blue' : 'var(--color-main-text)'" />
</template> </template>
</NcButton> </NcButton>
<NcButton v-if="!isLiked" <NcButton v-if="!isLiked"
type="tertiary-no-background"
v-tooltip="t('social', 'Like')" v-tooltip="t('social', 'Like')"
type="tertiary-no-background"
@click="like"> @click="like">
<template #icon> <template #icon>
<HeartOutline :size="20" /> <HeartOutline :size="20" />
</template> </template>
</NcButton> </NcButton>
<NcButton v-if="isLiked" <NcButton v-if="isLiked"
type="tertiary-no-background"
v-tooltip="t('social', 'Undo Like')" v-tooltip="t('social', 'Undo Like')"
type="tertiary-no-background"
@click="like"> @click="like">
<template #icon> <template #icon>
<Heart :size="20" :fill-color="'var(--color-error)'" /> <Heart :size="20" :fill-color="'var(--color-error)'" />
@ -68,8 +68,8 @@
</NcButton> </NcButton>
<NcActions> <NcActions>
<NcActionButton v-if="item.actor_info.account === cloudId" <NcActionButton v-if="item.actor_info.account === cloudId"
@click="remove()" icon="icon-delete"
icon="icon-delete"> @click="remove()">
{{ t('social', 'Delete') }} {{ t('social', 'Delete') }}
</NcActionButton> </NcActionButton>
</NcActions> </NcActions>
@ -79,9 +79,11 @@
<script> <script>
import * as linkify from 'linkifyjs' import * as linkify from 'linkifyjs'
// eslint-disable-next-line
import pluginMention from 'linkifyjs/plugins/mention' import pluginMention from 'linkifyjs/plugins/mention'
// eslint-disable-next-line
import 'linkifyjs/string' import 'linkifyjs/string'
import currentUser from './../mixins/currentUserMixin' import currentUser from './../mixins/currentUserMixin.js'
import PostAttachment from './PostAttachment.vue' import PostAttachment from './PostAttachment.vue'
import NcButton from '@nextcloud/vue/dist/Components/NcButton.js' import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
import NcActions from '@nextcloud/vue/dist/Components/NcActions.js' import NcActions from '@nextcloud/vue/dist/Components/NcActions.js'
@ -90,8 +92,7 @@ import Repeat from 'vue-material-design-icons/Repeat.vue'
import Reply from 'vue-material-design-icons/Reply.vue' import Reply from 'vue-material-design-icons/Reply.vue'
import Heart from 'vue-material-design-icons/Heart.vue' 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 '../logger' import logger from '../services/logger.js'
import MessageContent from './MessageContent'
import moment from '@nextcloud/moment' import moment from '@nextcloud/moment'
import { generateUrl } from '@nextcloud/router' import { generateUrl } from '@nextcloud/router'
import RichText from '@nextcloud/vue-richtext' import RichText from '@nextcloud/vue-richtext'
@ -102,7 +103,6 @@ export default {
name: 'TimelinePost', name: 'TimelinePost',
components: { components: {
PostAttachment, PostAttachment,
MessageContent,
NcActions, NcActions,
NcActionButton, NcActionButton,
NcButton, NcButton,
@ -115,7 +115,7 @@ export default {
mixins: [currentUser], mixins: [currentUser],
props: { props: {
item: { type: Object, default: () => {} }, item: { type: Object, default: () => {} },
parentAnnounce: { type: Object, default: () => {} } parentAnnounce: { type: Object, default: () => {} },
}, },
computed: { computed: {
relativeTimestamp() { relativeTimestamp() {
@ -157,6 +157,7 @@ export default {
}, },
methods: { methods: {
/** /**
* @param e
* @function getSinglePostTimeline * @function getSinglePostTimeline
* @description Opens the timeline of the post clicked * @description Opens the timeline of the post clicked
*/ */
@ -168,16 +169,17 @@ export default {
} else if (this.item.type === 'Announce') { } else if (this.item.type === 'Announce') {
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.item.type, { post: this.item })
} }
} else { } else {
this.$router.push({ name: 'single-post', this.$router.push({
name: 'single-post',
params: { params: {
account: this.item.actor_info.preferredUsername, account: this.item.actor_info.preferredUsername,
id: this.item.id, id: this.item.id,
localId: this.item.id.split('/')[this.item.id.split('/').length - 1], localId: this.item.id.split('/')[this.item.id.split('/').length - 1],
type: 'single-post' type: 'single-post',
} },
}) })
} }
}, },
@ -189,9 +191,9 @@ export default {
this.$root.$emit('composer-reply', this.item) this.$root.$emit('composer-reply', this.item)
}, },
boost() { boost() {
let params = { const params = {
post: this.item, post: this.item,
parentAnnounce: this.parentAnnounce parentAnnounce: this.parentAnnounce,
} }
if (this.isBoosted) { if (this.isBoosted) {
this.$store.dispatch('postUnBoost', params) this.$store.dispatch('postUnBoost', params)
@ -203,17 +205,17 @@ export default {
this.$store.dispatch('postDelete', this.item) this.$store.dispatch('postDelete', this.item)
}, },
like() { like() {
let params = { const params = {
post: this.item, post: this.item,
parentAnnounce: this.parentAnnounce parentAnnounce: this.parentAnnounce,
} }
if (this.isLiked) { if (this.isLiked) {
this.$store.dispatch('postUnlike', params) this.$store.dispatch('postUnlike', params)
} else { } else {
this.$store.dispatch('postLike', params) this.$store.dispatch('postLike', params)
} }
} },
} },
} }
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">

Wyświetl plik

@ -24,7 +24,9 @@
<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" :size="32" :user="item.preferredUsername" <NcAvatar v-if="item.local"
:size="32"
:user="item.preferredUsername"
:disable-tooltip="true" /> :disable-tooltip="true" />
<NcAvatar v-else :url="avatarUrl" /> <NcAvatar v-else :url="avatarUrl" />
</div> </div>
@ -37,7 +39,9 @@
{{ item.account }} {{ item.account }}
</span> </span>
</router-link> </router-link>
<a v-else :href="item.id" target="_blank" <a v-else
:href="item.id"
target="_blank"
rel="noreferrer"> rel="noreferrer">
<span class="post-author"> <span class="post-author">
{{ item.name }} {{ item.name }}
@ -49,7 +53,7 @@
<!-- 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.summary" />
</div> </div>
<follow-button :account="item.account" /> <FollowButton :account="item.account" />
</div> </div>
</div> </div>
</template> </template>
@ -69,14 +73,14 @@ export default {
}, },
mixins: [ mixins: [
follow, follow,
currentUser currentUser,
], ],
props: { props: {
item: { type: Object, default: () => {} } item: { type: Object, default: () => {} },
}, },
data: function() { data() {
return { return {
followingText: t('social', 'Following') followingText: t('social', 'Following'),
} }
}, },
computed: { computed: {
@ -88,8 +92,8 @@ export default {
}, },
avatarUrl() { avatarUrl() {
return generateUrl('/apps/social/api/v1/global/actor/avatar?id=' + this.id) return generateUrl('/apps/social/api/v1/global/actor/avatar?id=' + this.id)
} },
} },
} }
</script> </script>
<style scoped> <style scoped>

Wyświetl plik

@ -27,7 +27,7 @@ document.addEventListener('DOMContentLoaded', function() {
OCA.Dashboard.register('social_notifications', (el, { widget }) => { OCA.Dashboard.register('social_notifications', (el, { widget }) => {
const View = Vue.extend(Dashboard) const View = Vue.extend(Dashboard)
new View({ new View({
propsData: { title: widget.title } propsData: { title: widget.title },
}).$mount(el) }).$mount(el)
}) })
}) })

Wyświetl plik

@ -23,9 +23,9 @@
import Vue from 'vue' import Vue from 'vue'
export default { export default {
bind: function(el) { bind(el) {
Vue.nextTick(() => { Vue.nextTick(() => {
el.focus() el.focus()
}) })
} },
} }

Wyświetl plik

@ -24,12 +24,12 @@ import Vue from 'vue'
import { sync } from 'vuex-router-sync' import { sync } from 'vuex-router-sync'
import App from './App.vue' import App from './App.vue'
import store from './store' import store from './store/index.js'
import router from './router' import router from './router.js'
import vuetwemoji from 'vue-twemoji' import vuetwemoji from 'vue-twemoji'
import contenteditableDirective from 'vue-contenteditable-directive' 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' import VTooltip from '@nextcloud/vue/dist/Directives/Tooltip.js'
sync(store, router) sync(store, router)
@ -54,12 +54,12 @@ 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
}) })
/* eslint-disable-next-line no-new */ /* eslint-disable-next-line no-new */
new Vue({ new Vue({
router: router, router,
render: h => h(App), render: h => h(App),
store: store store,
}).$mount('#content') }).$mount('#content')

Wyświetl plik

@ -24,11 +24,11 @@
* *
*/ */
import serverData from './serverData' import serverData from './serverData.js'
export default { export default {
mixins: [ mixins: [
serverData serverData,
], ],
computed: { computed: {
/** @function Returns the complete account name */ /** @function Returns the complete account name */
@ -39,10 +39,12 @@ export default {
accountInfo() { accountInfo() {
return this.$store.getters.getAccount(this.profileAccount) return this.$store.getters.getAccount(this.profileAccount)
}, },
/** @function 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 */ * @function 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
*/
accountLoaded() { accountLoaded() {
return this.$store.getters.accountLoaded(this.profileAccount) return this.$store.getters.accountLoaded(this.profileAccount)
} },
} },
} }

Wyświetl plik

@ -20,10 +20,10 @@
* *
*/ */
import serverData from './serverData' import serverData from './serverData.js'
export default { export default {
mixins: [ mixins: [
serverData serverData,
], ],
computed: { computed: {
currentUser() { currentUser() {
@ -34,6 +34,6 @@ export default {
}, },
cloudId() { cloudId() {
return this.currentUser.uid + '@' + this.hostname return this.currentUser.uid + '@' + this.hostname
} },
} },
} }

Wyświetl plik

@ -34,7 +34,7 @@ class UnfollowException {
export default { export default {
data() { data() {
return { return {
followLoading: false followLoading: false,
} }
}, },
methods: { methods: {
@ -66,6 +66,6 @@ export default {
OC.Notification.showTemporary(`Failed to unfollow user ${this.item.account}`) OC.Notification.showTemporary(`Failed to unfollow user ${this.item.account}`)
console.error(`Failed to unfollow user ${this.item.account}`, error.response.data) console.error(`Failed to unfollow user ${this.item.account}`, error.response.data)
}) })
} },
} },
} }

Wyświetl plik

@ -20,15 +20,15 @@
* *
*/ */
import PopoverMenu from '@nextcloud/vue/dist/Components/PopoverMenu' import NcPopoverMenu from '@nextcloud/vue/dist/Components/NcPopoverMenu.js'
export default { export default {
components: { components: {
PopoverMenu NcPopoverMenu,
}, },
data() { data() {
return { return {
menuOpened: false menuOpened: false,
} }
}, },
methods: { methods: {
@ -37,6 +37,6 @@ export default {
}, },
hidePopoverMenu() { hidePopoverMenu() {
this.menuOpened = false this.menuOpened = false
} },
} },
} }

Wyświetl plik

@ -26,15 +26,16 @@
export default { export default {
computed: { computed: {
/** @description Returns the serverData object /**
* @property {String} account - The account that the user wants to follow (Only in 'OStatus.vue') * @description Returns the serverData object
* @property {string} account - The account that the user wants to follow (Only in 'OStatus.vue')
* @property cliUrl * @property cliUrl
* @property cloudAddress * @property cloudAddress
* @property firstrun * @property firstrun
* @property isAdmin * @property isAdmin
* @property {String} local - The local part of the account that the user wants to follow * @property {string} local - The local part of the account that the user wants to follow
* @property {boolean} public - False when the page is accessed by an authenticated user. True otherwise * @property {boolean} public - False when the page is accessed by an authenticated user. True otherwise
* @property setup * @property setup
*/ */
serverData() { serverData() {
if (!this.$store) { if (!this.$store) {
@ -46,6 +47,6 @@ export default {
const url = document.createElement('a') const url = document.createElement('a')
url.setAttribute('href', this.serverData.cloudAddress) url.setAttribute('href', this.serverData.cloudAddress)
return url.hostname return url.hostname
} },
} },
} }

Wyświetl plik

@ -21,7 +21,7 @@
*/ */
import Vue from 'vue' import Vue from 'vue'
import store from './store' import store from './store/index.js'
import OStatus from './views/OStatus.vue' import OStatus from './views/OStatus.vue'
// eslint-disable-next-line // eslint-disable-next-line
@ -37,5 +37,5 @@ Vue.prototype.OCA = OCA
/* eslint-disable-next-line no-new */ /* eslint-disable-next-line no-new */
new Vue({ new Vue({
render: h => h(OStatus), render: h => h(OStatus),
store: store store,
}).$mount('#content') }).$mount('#content')

Wyświetl plik

@ -2,25 +2,25 @@
// SPDX-License-Identifier: AGPL-3.0-or-later // SPDX-License-Identifier: AGPL-3.0-or-later
// eslint-disable-next-line // eslint-disable-next-line
__webpack_nonce__ = btoa(OC.requestToken)
// eslint-disable-next-line
__webpack_public_path__ = OC.linkTo('social', 'js/')
import ProfilePageIntegration from './views/ProfilePageIntegration.vue' import ProfilePageIntegration from './views/ProfilePageIntegration.vue'
import Vue from 'vue' import Vue from 'vue'
import { sync } from 'vuex-router-sync' import { generateFilePath } from '@nextcloud/router'
import { translate, translatePlural } from '@nextcloud/l10n'
if (!OCA?.Core?.ProfileSections) { // eslint-disable-next-line
exit(); __webpack_nonce__ = btoa(OC.requestToken)
// eslint-disable-next-line
__webpack_public_path__ = generateFilePath('social', '', 'js/')
if (OCA?.Core?.ProfileSections) {
Vue.prototype.t = translate
Vue.prototype.n = translatePlural
Vue.prototype.OC = OC
Vue.prototype.OCA = OCA
const View = Vue.extend(ProfilePageIntegration)
OCA.Core.ProfileSections.registerSection((el, userId) => {
return View
})
} }
Vue.prototype.t = t
Vue.prototype.n = n
Vue.prototype.OC = OC
Vue.prototype.OCA = OCA
const View = Vue.extend(ProfilePageIntegration)
OCA.Core.ProfileSections.registerSection((el, userId) => {
return View
})

Wyświetl plik

@ -43,35 +43,35 @@ export default new Router({
routes: [ routes: [
{ {
path: '/:index(index.php/)?apps/social/', path: '/:index(index.php/)?apps/social/',
redirect: { name: 'timeline' } redirect: { name: 'timeline' },
}, },
{ {
path: '/:index(index.php/)?apps/social/timeline/:type?', path: '/:index(index.php/)?apps/social/timeline/:type?',
components: { components: {
default: Timeline default: Timeline,
}, },
props: true, props: true,
name: 'timeline', name: 'timeline',
children: [ children: [
{ {
path: 'tags/:tag', path: 'tags/:tag',
name: 'tags' name: 'tags',
} },
] ],
}, },
{ {
path: '/:index(index.php/)?apps/social/@:account/:localId', path: '/:index(index.php/)?apps/social/@:account/:localId',
components: { components: {
default: TimelineSinglePost default: TimelineSinglePost,
}, },
props: true, props: true,
name: 'single-post' name: 'single-post',
}, },
{ {
path: '/:index(index.php/)?apps/social/@:account', path: '/:index(index.php/)?apps/social/@:account',
components: { components: {
default: Profile, default: Profile,
details: ProfileTimeline details: ProfileTimeline,
}, },
props: true, props: true,
children: [ children: [
@ -79,33 +79,33 @@ export default new Router({
path: '', path: '',
name: 'profile', name: 'profile',
components: { components: {
details: ProfileTimeline details: ProfileTimeline,
} },
}, },
{ {
path: 'followers', path: 'followers',
name: 'profile.followers', name: 'profile.followers',
components: { components: {
details: ProfileFollowers details: ProfileFollowers,
} },
}, },
{ {
path: 'following', path: 'following',
name: 'profile.following', name: 'profile.following',
components: { components: {
details: ProfileFollowers details: ProfileFollowers,
} },
} },
] ],
}, },
{ {
path: '/:index(index.php/)?apps/social/ostatus/follow', path: '/:index(index.php/)?apps/social/ostatus/follow',
components: { components: {
default: Profile, default: Profile,
details: ProfileTimeline details: ProfileTimeline,
}, },
props: true props: true,
} },
] ],
}) })

Wyświetl plik

@ -0,0 +1,37 @@
/**
* @copyright 2022 Louis Chemineau <mlouis@chmn.me>
*
* @author Louis Chemineau <mlouis@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/>.
*/
import { getCurrentUser } from '@nextcloud/auth'
import { getLoggerBuilder } from '@nextcloud/logger'
const getLogger = user => {
if (user === null) {
return getLoggerBuilder()
.setApp('social')
.build()
}
return getLoggerBuilder()
.setApp('social')
.setUid(user.uid)
.build()
}
export default getLogger(getCurrentUser())

Wyświetl plik

@ -27,7 +27,7 @@ import { generateUrl } from '@nextcloud/router'
const state = { const state = {
currentAccount: {}, currentAccount: {},
accounts: {}, accounts: {},
accountIdMap: {} accountIdMap: {},
} }
const addAccount = (state, { actorId, data }) => { const addAccount = (state, { actorId, data }) => {
Vue.set(state.accounts, actorId, Object.assign({ followersList: [], followingList: [], details: { following: false, follower: false } }, state.accounts[actorId], data)) Vue.set(state.accounts, actorId, Object.assign({ followersList: [], followingList: [], details: { following: false, follower: false } }, state.accounts[actorId], data))
@ -43,25 +43,25 @@ const mutations = {
addAccount(state, { actorId, data }) addAccount(state, { actorId, data })
}, },
addFollowers(state, { account, data }) { addFollowers(state, { account, data }) {
let users = [] const users = []
for (var index in data) { for (const index in data) {
const actor = data[index].actor_info const actor = data[index].actor_info
addAccount(state, { addAccount(state, {
actorId: actor.id, actorId: actor.id,
data: actor data: actor,
}) })
} }
Vue.set(state.accounts[_getActorIdForAccount(account)], 'followersList', users) Vue.set(state.accounts[_getActorIdForAccount(account)], 'followersList', users)
}, },
addFollowing(state, { account, data }) { addFollowing(state, { account, data }) {
let users = [] const users = []
for (var index in data) { for (const index in data) {
const actor = data[index].actor_info const actor = data[index].actor_info
if (typeof actor !== 'undefined' && account !== actor.account) { if (typeof actor !== 'undefined' && account !== actor.account) {
users.push(actor.id) users.push(actor.id)
addAccount(state, { addAccount(state, {
actorId: actor.id, actorId: actor.id,
data: actor data: actor,
}) })
} }
} }
@ -72,7 +72,7 @@ const mutations = {
}, },
unfollowAccount(state, accountToUnfollow) { unfollowAccount(state, accountToUnfollow) {
Vue.set(state.accounts[_getActorIdForAccount(accountToUnfollow)].details, 'following', false) Vue.set(state.accounts[_getActorIdForAccount(accountToUnfollow)].details, 'following', false)
} },
} }
const getters = { const getters = {
@ -101,10 +101,10 @@ const getters = {
}, },
isFollowingUser(state) { isFollowingUser(state) {
return (followingAccount) => { return (followingAccount) => {
let account = state.accounts[_getActorIdForAccount(followingAccount)] const account = state.accounts[_getActorIdForAccount(followingAccount)]
return account && account.details ? account.details.following : false return account && account.details ? account.details.following : false
} }
} },
} }
const actions = { const actions = {
@ -169,7 +169,7 @@ const actions = {
axios.get(generateUrl(`apps/social/api/v1/account/${uid}/following`)).then((response) => { axios.get(generateUrl(`apps/social/api/v1/account/${uid}/following`)).then((response) => {
context.commit('addFollowing', { account, data: response.data.result }) context.commit('addFollowing', { account, data: response.data.result })
}) })
} },
} }
export default { state, mutations, getters, actions } export default { state, mutations, getters, actions }

Wyświetl plik

@ -23,9 +23,9 @@
import Vue from 'vue' import Vue from 'vue'
import Vuex from 'vuex' import Vuex from 'vuex'
import timeline from './timeline' import timeline from './timeline.js'
import account from './account' import account from './account.js'
import settings from './settings' import settings from './settings.js'
Vue.use(Vuex) Vue.use(Vuex)
@ -35,7 +35,7 @@ export default new Vuex.Store({
modules: { modules: {
timeline, timeline,
account, account,
settings settings,
}, },
strict: debug strict: debug,
}) })

Wyświetl plik

@ -21,7 +21,7 @@
*/ */
const state = { const state = {
serverData: {} serverData: {},
} }
const mutations = { const mutations = {
setServerData(state, data) { setServerData(state, data) {
@ -29,12 +29,12 @@ const mutations = {
}, },
setServerDataEntry(state, key, value) { setServerDataEntry(state, key, value) {
state.serverData[key] = value state.serverData[key] = value
} },
} }
const getters = { const getters = {
getServerData(state) { getServerData(state) {
return state.serverData return state.serverData
} },
} }
const actions = {} const actions = {}

Wyświetl plik

@ -23,17 +23,17 @@
* *
*/ */
import Logger from '../logger' import logger from '../services/logger.js'
import axios from '@nextcloud/axios' import axios from '@nextcloud/axios'
import Vue from 'vue' import Vue from 'vue'
import { generateUrl } from '@nextcloud/router' import { generateUrl } from '@nextcloud/router'
/** /**
* @property {object} timeline - The posts' collection * @property {object} timeline - The posts' collection
* @property {int} since - Time (EPOCH) of the most recent post * @property {int} since - Time (EPOCH) of the most recent post
* @property {string} type - Timeline's type: 'home', 'single-post',... * @property {string} type - Timeline's type: 'home', 'single-post',...
* @property {object} params - Timeline's parameters * @property {object} params - Timeline's parameters
* @property {string} account - * @property {string} account -
*/ */
const state = { const state = {
timeline: {}, timeline: {},
@ -41,10 +41,10 @@ const state = {
type: 'home', type: 'home',
/** /**
* @namespace params * @namespace params
* @property {string} account ??? * @property {string} account ???
* @property {string} id * @property {string} id
* @property {string} localId * @property {string} localId
* @property {string} type ??? * @property {string} type ???
*/ */
params: {}, params: {},
account: '', account: '',
@ -52,11 +52,11 @@ const state = {
* 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,
} }
const mutations = { const mutations = {
addToTimeline(state, data) { addToTimeline(state, data) {
for (let item in data) { for (const item in data) {
state.since = data[item].publishedTime state.since = data[item].publishedTime
Vue.set(state.timeline, data[item].id, data[item]) Vue.set(state.timeline, data[item].id, data[item])
} }
@ -111,7 +111,7 @@ const mutations = {
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.action.values, 'boosted', false)
} }
} },
} }
const getters = { const getters = {
getComposerDisplayStatus(state) { getComposerDisplayStatus(state) {
@ -127,10 +127,10 @@ const getters = {
if (typeof state.timeline[postId] !== 'undefined') { if (typeof state.timeline[postId] !== 'undefined') {
return state.timeline[postId] return state.timeline[postId]
} else { } else {
Logger.warn('Could not find post in timeline', { postId: postId }) logger.warn('Could not find post in timeline', { postId })
} }
} }
} },
} }
const actions = { const actions = {
changeTimelineType(context, { type, params }) { changeTimelineType(context, { type, params }) {
@ -148,22 +148,22 @@ const actions = {
try { try {
const { data } = await axios.post(generateUrl('apps/social/api/v1/post'), post, { const { data } = await axios.post(generateUrl('apps/social/api/v1/post'), post, {
headers: { headers: {
'Content-Type': 'multipart/form-data' 'Content-Type': 'multipart/form-data',
} },
}) })
Logger.info('Post created with token ' + data.result.token) logger.info('Post created with token ' + data.result.token)
} catch (error) { } catch (error) {
OC.Notification.showTemporary('Failed to create a post') OC.Notification.showTemporary('Failed to create a post')
Logger.error('Failed to create a post', { 'error': error.response }) logger.error('Failed to create a post', { error: error.response })
} }
}, },
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.id}`)).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') OC.Notification.showTemporary('Failed to delete the post')
Logger.error('Failed to delete the post', { 'error': error }) logger.error('Failed to delete the post', { error })
}) })
}, },
postLike(context, { post, parentAnnounce }) { postLike(context, { post, parentAnnounce }) {
@ -173,7 +173,7 @@ const actions = {
resolve(response) resolve(response)
}).catch((error) => { }).catch((error) => {
OC.Notification.showTemporary('Failed to like post') OC.Notification.showTemporary('Failed to like post')
Logger.error('Failed to like post', { 'error': error.response }) logger.error('Failed to like post', { error: error.response })
reject(error) reject(error)
}) })
}) })
@ -187,18 +187,18 @@ const actions = {
} }
}).catch((error) => { }).catch((error) => {
OC.Notification.showTemporary('Failed to unlike post') OC.Notification.showTemporary('Failed to unlike post')
Logger.error('Failed to unlike post', { 'error': error }) logger.error('Failed to unlike post', { error })
}) })
}, },
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.id}`)).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') OC.Notification.showTemporary('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: error.response })
reject(error) reject(error)
}) })
}) })
@ -206,10 +206,10 @@ const actions = {
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.id}`)).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') OC.Notification.showTemporary('Failed to delete the boost')
Logger.error('Failed to delete the boost', { 'error': error }) logger.error('Failed to delete the boost', { error })
}) })
}, },
refreshTimeline(context) { refreshTimeline(context) {
@ -248,7 +248,7 @@ const actions = {
}, },
addToTimeline(context, data) { addToTimeline(context, data) {
context.commit('addToTimeline', data) context.commit('addToTimeline', data)
} },
} }
export default { state, mutations, getters, actions } export default { state, mutations, getters, actions }

Wyświetl plik

@ -26,8 +26,7 @@
:show-more-text="title" :show-more-text="title"
:loading="state === 'loading'"> :loading="state === 'loading'">
<template #empty-content> <template #empty-content>
<NcEmptyContent <NcEmptyContent v-if="emptyContentMessage"
v-if="emptyContentMessage"
:icon="emptyContentIcon"> :icon="emptyContentIcon">
<template #desc> <template #desc>
{{ emptyContentMessage }} {{ emptyContentMessage }}
@ -54,14 +53,14 @@ export default {
components: { components: {
NcDashboardWidget, NcDashboardWidget,
NcEmptyContent NcEmptyContent,
}, },
props: { props: {
title: { title: {
type: String, type: String,
required: true required: true,
} },
}, },
data() { data() {
@ -71,7 +70,7 @@ export default {
showMoreText: t('social', 'Social notifications'), showMoreText: t('social', 'Social notifications'),
loop: null, loop: null,
state: 'loading', state: 'loading',
appUrl: generateUrl('/apps/social') appUrl: generateUrl('/apps/social'),
} }
}, },
@ -85,7 +84,7 @@ export default {
avatarUsername: this.getActorName(n), avatarUsername: this.getActorName(n),
overlayIconUrl: this.getNotificationTypeImage(n), overlayIconUrl: this.getNotificationTypeImage(n),
mainText: this.getMainText(n), mainText: this.getMainText(n),
subText: this.getSubline(n) subText: this.getSubline(n),
} }
}) })
}, },
@ -109,7 +108,7 @@ export default {
return 'icon-checkmark' return 'icon-checkmark'
} }
return 'icon-checkmark' return 'icon-checkmark'
} },
}, },
beforeMount() { beforeMount() {
@ -121,8 +120,8 @@ export default {
fetchNotifications() { fetchNotifications() {
const req = { const req = {
params: { params: {
limit: 10 limit: 10,
} },
} }
const url = generateUrl('/apps/social/api/v1/stream/notifications') const url = generateUrl('/apps/social/api/v1/stream/notifications')
// TODO check why 'since' param is in fact 'until' // TODO check why 'since' param is in fact 'until'
@ -215,8 +214,8 @@ export default {
return generateUrl('/svg/social/add_user') return generateUrl('/svg/social/add_user')
} }
return '' return ''
} },
} },
} }
</script> </script>

Wyświetl plik

@ -38,28 +38,6 @@
<div v-else :class="{ 'icon-loading-dark': !account }" /> <div v-else :class="{ 'icon-loading-dark': !account }" />
</template> </template>
<style scoped>
h2, p {
color: var(--color-primary-text);
}
p .icon {
display: inline-block;
}
.avatardiv {
vertical-align: -4px;
margin-right: 3px;
filter: drop-shadow(0 0 0.5rem #333);
margin-top: 10px;
margin-bottom: 20px;
}
</style>
<style>
.wrapper {
margin-top: 20px;
}
</style>
<script> <script>
import NcAvatar from '@nextcloud/vue/dist/Components/NcAvatar.js' import NcAvatar from '@nextcloud/vue/dist/Components/NcAvatar.js'
import axios from '@nextcloud/axios' import axios from '@nextcloud/axios'
@ -69,18 +47,18 @@ import { loadState } from '@nextcloud/initial-state'
import { generateUrl } from '@nextcloud/router' import { generateUrl } from '@nextcloud/router'
export default { export default {
name: 'App', name: 'OStatus',
components: { components: {
NcAvatar, NcAvatar,
}, },
mixins: [ mixins: [
accountMixins, accountMixins,
currentuserMixin currentuserMixin,
], ],
data() { data() {
return { return {
remote: '', remote: '',
account: {} account: {},
} }
}, },
computed: { computed: {
@ -99,9 +77,9 @@ export default {
} }
return (this.account.name ? this.account.name : this.account.preferredUsername) return (this.account.name ? this.account.name : this.account.preferredUsername)
} },
}, },
beforeMount: function() { beforeMount() {
// importing server data into the store and fetching viewed account's information // importing server data into the store and fetching viewed account's information
try { try {
const serverData = loadState('social', 'serverData') const serverData = loadState('social', 'serverData')
@ -136,7 +114,29 @@ export default {
}, },
close() { close() {
window.close() window.close()
} },
} },
} }
</script> </script>
<style scoped>
h2, p {
color: var(--color-primary-text);
}
p .icon {
display: inline-block;
}
.avatardiv {
vertical-align: -4px;
margin-right: 3px;
filter: drop-shadow(0 0 0.5rem #333);
margin-top: 10px;
margin-bottom: 20px;
}
</style>
<style>
.wrapper {
margin-top: 20px;
}
</style>

Wyświetl plik

@ -22,12 +22,12 @@
<template> <template>
<div :class="{'icon-loading': !accountLoaded}" class="social__wrapper"> <div :class="{'icon-loading': !accountLoaded}" class="social__wrapper">
<profile-info 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 && accountInfo.local" 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: this.uid })"> :description="t('social', 'Sorry, we could not find the account of {userId}', { userId: uid })">
<template #icon> <template #icon>
<img :src="emptyContentImage" <img :src="emptyContentImage"
class="icon-illustration" class="icon-illustration"
@ -37,14 +37,6 @@
</div> </div>
</template> </template>
<style scoped>
.social__wrapper.icon-loading {
margin-top: 50vh;
}
</style>
<script> <script>
import ProfileInfo from './../components/ProfileInfo.vue' import ProfileInfo from './../components/ProfileInfo.vue'
import NcEmptyContent from '@nextcloud/vue/dist/Components/NcEmptyContent.js' import NcEmptyContent from '@nextcloud/vue/dist/Components/NcEmptyContent.js'
@ -56,20 +48,20 @@ export default {
name: 'Profile', name: 'Profile',
components: { components: {
NcEmptyContent, NcEmptyContent,
ProfileInfo ProfileInfo,
}, },
mixins: [ mixins: [
accountMixins, accountMixins,
serverData serverData,
], ],
data() { data() {
return { return {
state: [], state: [],
uid: null uid: null,
} }
}, },
computed: { computed: {
timeline: function() { timeline() {
return this.$store.getters.getTimeline return this.$store.getters.getTimeline
}, },
emptyContentImage() { emptyContentImage() {
@ -93,6 +85,14 @@ export default {
this.$store.dispatch(fetchMethod, this.profileAccount).then((response) => { this.$store.dispatch(fetchMethod, this.profileAccount).then((response) => {
this.uid = response.account this.uid = response.account
}) })
} },
} }
</script> </script>
<style scoped>
.social__wrapper.icon-loading {
margin-top: 50vh;
}
</style>

Wyświetl plik

@ -22,10 +22,44 @@
<template> <template>
<div class="social__followers"> <div class="social__followers">
<user-entry v-for="user in users" :key="user.id" :item="user" /> <UserEntry v-for="user in users" :key="user.id" :item="user" />
</div> </div>
</template> </template>
<script>
import UserEntry from '../components/UserEntry.vue'
import serverData from '../mixins/serverData.js'
export default {
name: 'ProfileFollowers',
components: {
UserEntry,
},
mixins: [
serverData,
],
computed: {
profileAccount() {
return (this.$route.params.account.indexOf('@') === -1) ? this.$route.params.account + '@' + this.hostname : this.$route.params.account
},
users() {
if (this.$route.name === 'profile.followers') {
return this.$store.getters.getAccountFollowers(this.profileAccount)
} else {
return this.$store.getters.getAccountFollowing(this.profileAccount)
}
},
},
beforeMount() {
if (this.$route.name === 'profile.followers') {
this.$store.dispatch('fetchAccountFollowers', this.profileAccount)
} else {
this.$store.dispatch('fetchAccountFollowing', this.profileAccount)
}
},
}
</script>
<style scoped> <style scoped>
.social__followers { .social__followers {
width: 100%; width: 100%;
@ -40,37 +74,3 @@
margin-bottom: 10px; margin-bottom: 10px;
} }
</style> </style>
<script>
import UserEntry from '../components/UserEntry.vue'
import serverData from '../mixins/serverData'
export default {
name: 'ProfileFollowers',
components: {
UserEntry
},
mixins: [
serverData
],
computed: {
profileAccount() {
return (this.$route.params.account.indexOf('@') === -1) ? this.$route.params.account + '@' + this.hostname : this.$route.params.account
},
users: function() {
if (this.$route.name === 'profile.followers') {
return this.$store.getters.getAccountFollowers(this.profileAccount)
} else {
return this.$store.getters.getAccountFollowing(this.profileAccount)
}
}
},
beforeMount() {
if (this.$route.name === 'profile.followers') {
this.$store.dispatch('fetchAccountFollowers', this.profileAccount)
} else {
this.$store.dispatch('fetchAccountFollowing', this.profileAccount)
}
}
}
</script>

Wyświetl plik

@ -2,19 +2,24 @@
<div> <div>
<h2>Social</h2> <h2>Social</h2>
<transition-group name="list" tag="div"> <transition-group name="list" tag="div">
<TimelineEntry v-for="entry in timeline" :key="entry.id" :item="entry" :isProfilePage="true" /> <TimelineEntry v-for="entry in timeline"
:key="entry.id"
:item="entry" />
</transition-group> </transition-group>
</div> </div>
</template> </template>
<script> <script>
import ProfileInfo from './../components/ProfileInfo.vue'
import TimelineEntry from './../components/TimelineEntry.vue' import TimelineEntry from './../components/TimelineEntry.vue'
import axios from '@nextcloud/axios' import axios from '@nextcloud/axios'
import { generateUrl } from '@nextcloud/router' import { generateUrl } from '@nextcloud/router'
import logger from './../services/logger.js'
export default { export default {
name: 'ProfilePageIntegration', name: 'ProfilePageIntegration',
components: {
TimelineEntry,
},
props: { props: {
userId: { userId: {
type: String, type: String,
@ -27,33 +32,27 @@ export default {
timeline: [], timeline: [],
} }
}, },
components: {
ProfileInfo,
TimelineEntry,
},
computed: { computed: {
getCount() { getCount() {
let account = this.accountInfo const account = this.accountInfo
return (field) => account.details.count ? account.details.count[field] : '' return (field) => account.details.count ? account.details.count[field] : ''
}, },
}, },
// Start fetching account information before mounting the component // Start fetching account information before mounting the component
beforeMount() { beforeMount() {
let fetchMethod = 'fetchPublicAccountInfo' const uid = this.userId
let uid = this.userId
axios.get(generateUrl(`apps/social/api/v1/account/${uid}/info`)).then((response) => { axios.get(generateUrl(`apps/social/api/v1/account/${uid}/info`)).then((response) => {
this.accountInfo = response.data.result.account this.accountInfo = response.data.result.account
console.log(this.accountInfo) logger.log(this.accountInfo)
}) })
const since = Math.floor(Date.now() / 1000) + 1 const since = Math.floor(Date.now() / 1000) + 1
axios.get(generateUrl(`apps/social/api/v1/account/${uid}/stream?limit=25&since=${since}`)).then(({ data }) => { axios.get(generateUrl(`apps/social/api/v1/account/${uid}/stream?limit=25&since=${since}`)).then(({ data }) => {
console.log(this.timeline) logger.log(this.timeline)
this.timeline = data.result this.timeline = data.result
}) })
} },
} }
</script> </script>

Wyświetl plik

@ -21,26 +21,26 @@
--> -->
<template> <template>
<timeline-list /> <TimelineList />
</template> </template>
<style scoped>
</style>
<script> <script>
import TimelineList from './../components/TimelineList.vue' import TimelineList from './../components/TimelineList.vue'
export default { export default {
name: 'ProfileTimeline', name: 'ProfileTimeline',
components: { components: {
TimelineList TimelineList,
}, },
computed: { computed: {
}, },
beforeMount: function() { beforeMount() {
this.$store.dispatch('changeTimelineTypeAccount', this.$route.params.account) this.$store.dispatch('changeTimelineTypeAccount', this.$route.params.account)
} },
} }
</script> </script>
<style scoped>
</style>

Wyświetl plik

@ -16,19 +16,88 @@
</p> </p>
<div v-show="!isFollowingNextcloudAccount" class="follow-nextcloud"> <div v-show="!isFollowingNextcloudAccount" class="follow-nextcloud">
<p>{{ t('social', 'Since you are new to Social, start by following the official Nextcloud account so you don\'t miss any news') }}</p> <p>{{ t('social', 'Since you are new to Social, start by following the official Nextcloud account so you don\'t miss any news') }}</p>
<input :value="t('social', 'Follow Nextcloud on mastodon.xyz')" type="button" class="primary" <input :value="t('social', 'Follow Nextcloud on mastodon.xyz')"
type="button"
class="primary"
@click="followNextcloud"> @click="followNextcloud">
</div> </div>
</div> </div>
</transition> </transition>
<composer v-if="type !== 'notifications' && type !== 'single-post'" /> <Composer v-if="type !== 'notifications' && type !== 'single-post'" />
<h2 v-if="type === 'tags'"> <h2 v-if="type === 'tags'">
#{{ this.$route.params.tag }} #{{ $route.params.tag }}
</h2> </h2>
<timeline-list :type="type" /> <TimelineList :type="type" />
</div> </div>
</template> </template>
<script>
import Composer from './../components/Composer/Composer.vue'
import CurrentUserMixin from './../mixins/currentUserMixin.js'
import follow from './../mixins/follow.js'
import TimelineList from './../components/TimelineList.vue'
export default {
name: 'Timeline',
components: {
Composer,
TimelineList,
},
mixins: [
CurrentUserMixin,
follow,
],
data() {
return {
infoHidden: false,
nextcloudAccount: 'nextcloud@mastodon.xyz',
}
},
computed: {
params() {
if (this.$route.name === 'tags') {
return { tag: this.$route.params.tag }
} else if (this.$route.name === 'single-post') {
return this.$route.params
}
return {}
},
type() {
if (this.$route.name === 'tags') {
return 'tags'
}
if (this.$route.params.type) {
return this.$route.params.type
}
return 'home'
},
showInfo() {
return this.$store.getters.getServerData.firstrun && !this.infoHidden
},
isFollowingNextcloudAccount() {
if (!this.$store.getters.accountLoaded(this.nextcloudAccount)) {
return true
}
return this.$store.getters.isFollowingUser(this.nextcloudAccount)
},
},
beforeMount() {
this.$store.dispatch('changeTimelineType', { type: this.type, params: this.params })
if (this.showInfo) {
this.$store.dispatch('fetchAccountInfo', this.nextcloudAccount)
}
},
methods: {
hideInfo() {
this.infoHidden = true
},
followNextcloud() {
this.$store.dispatch('followAccount', { accountToFollow: this.nextcloudAccount })
},
},
}
</script>
<style scoped> <style scoped>
.social__welcome { .social__welcome {
@ -88,70 +157,3 @@
} }
</style> </style>
<script>
import Composer from './../components/Composer/Composer.vue'
import CurrentUserMixin from './../mixins/currentUserMixin.js'
import follow from './../mixins/follow.js'
import TimelineList from './../components/TimelineList.vue'
export default {
name: 'Timeline',
components: {
Composer,
TimelineList
},
mixins: [
CurrentUserMixin,
follow
],
data: function() {
return {
infoHidden: false,
nextcloudAccount: 'nextcloud@mastodon.xyz'
}
},
computed: {
params() {
if (this.$route.name === 'tags') {
return { tag: this.$route.params.tag }
} else if (this.$route.name === 'single-post') {
return this.$route.params
}
return {}
},
type() {
if (this.$route.name === 'tags') {
return 'tags'
}
if (this.$route.params.type) {
return this.$route.params.type
}
return 'home'
},
showInfo() {
return this.$store.getters.getServerData.firstrun && !this.infoHidden
},
isFollowingNextcloudAccount() {
if (!this.$store.getters.accountLoaded(this.nextcloudAccount)) {
return true
}
return this.$store.getters.isFollowingUser(this.nextcloudAccount)
}
},
beforeMount: function() {
this.$store.dispatch('changeTimelineType', { type: this.type, params: this.params })
if (this.showInfo) {
this.$store.dispatch('fetchAccountInfo', this.nextcloudAccount)
}
},
methods: {
hideInfo() {
this.infoHidden = true
},
followNextcloud() {
this.$store.dispatch('followAccount', { accountToFollow: this.nextcloudAccount })
}
}
}
</script>

Wyświetl plik

@ -1,25 +1,12 @@
<template> <template>
<div class="social__wrapper"> <div class="social__wrapper">
<profile-info v-if="accountLoaded && accountInfo" :uid="uid" /> <ProfileInfo v-if="accountLoaded && accountInfo" :uid="uid" />
<composer v-show="composerDisplayStatus" /> <Composer v-show="composerDisplayStatus" />
<timeline-entry class="main-post" :item="mainPost" /> <TimelineEntry class="main-post" :item="mainPost" />
<timeline-list v-if="timeline" :type="$route.params.type" /> <TimelineList v-if="timeline" :type="$route.params.type" />
</div> </div>
</template> </template>
<style scoped>
.social__timeline {
max-width: 600px;
margin: 15px auto;
}
#app-content {
position: relative;
}
</style>
<script> <script>
import Composer from '../components/Composer/Composer.vue' import Composer from '../components/Composer/Composer.vue'
import ProfileInfo from '../components/ProfileInfo.vue' import ProfileInfo from '../components/ProfileInfo.vue'
@ -36,43 +23,43 @@ export default {
Composer, Composer,
ProfileInfo, ProfileInfo,
TimelineEntry, TimelineEntry,
TimelineList TimelineList,
}, },
mixins: [ mixins: [
accountMixins, accountMixins,
currentUserMixin, currentUserMixin,
serverData serverData,
], ],
data() { data() {
return { return {
mainPost: {}, mainPost: {},
uid: this.$route.params.account uid: this.$route.params.account,
} }
}, },
computed: { computed: {
/** /**
* @description Tells whether Composer shall be displayed or not * @description Tells whether Composer shall be displayed or not
* @returns {boolean} * @return {boolean}
*/ */
composerDisplayStatus() { composerDisplayStatus() {
return this.$store.getters.getComposerDisplayStatus return this.$store.getters.getComposerDisplayStatus
}, },
/** /**
* @description Extracts the viewed account name from the URL * @description Extracts the viewed account name from the URL
* @returns {String} * @return {string}
*/ */
account() { account() {
return window.location.href.split('/')[window.location.href.split('/').length - 2].slice(1) return window.location.href.split('/')[window.location.href.split('/').length - 2].slice(1)
}, },
/** /**
* @description Returns the timeline currently loaded in the store * @description Returns the timeline currently loaded in the store
* @returns {Object} * @return {object}
*/ */
timeline: function() { timeline() {
return this.$store.getters.getTimeline return this.$store.getters.getTimeline
} },
}, },
beforeMount: function() { beforeMount() {
// Get data of post clicked on // Get data of post clicked on
if (typeof this.$route.params.id === 'undefined') { if (typeof this.$route.params.id === 'undefined') {
@ -89,22 +76,35 @@ export default {
}) })
// Fetch single post timeline // Fetch single post timeline
let 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: window.location.href.split('/')[window.location.href.split('/').length - 1],
type: 'single-post' type: 'single-post',
} }
this.$store.dispatch('changeTimelineType', { this.$store.dispatch('changeTimelineType', {
type: 'single-post', type: 'single-post',
params: params params,
}) })
}, },
methods: { methods: {
} },
} }
</script> </script>
<style scoped>
.social__timeline {
max-width: 600px;
margin: 15px auto;
}
#app-content {
position: relative;
}
</style>
<style> <style>
</style> </style>