More stuff to commit

Signed-off-by: Carl Schwan <carl@carlschwan.eu>
pull/1439/head
Carl Schwan 2022-09-12 13:43:13 +02:00
rodzic a2fca565a1
commit ee1b2945bb
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: C3AA6B3A5EFA7AC5
12 zmienionych plików z 233 dodań i 180 usunięć

Wyświetl plik

@ -81,10 +81,16 @@ return [
['name' => 'Api#savedSearches', 'url' => '/api/saved_searches/list.json', 'verb' => 'GET'],
['name' => 'Api#timelines', 'url' => '/api/v1/timelines/{timeline}/', 'verb' => 'GET'],
['name' => 'Api#notifications', 'url' => '/api/v1/notifications', 'verb' => 'GET'],
['name' => 'MediaApi#uploadMedia', 'url' => '/api/v1/media', 'verb' => 'POST'],
['name' => 'MediaApi#updateMedia', 'url' => '/api/v1/media/{id}', 'verb' => 'PUT'],
['name' => 'MediaApi#deleteMedia', 'url' => '/api/v1/media/{id}', 'verb' => 'DELETE'],
['name' => 'MediaApi#getMedia', 'url' => '/media/{shortcode}.{extension}', 'verb' => 'GET'],
['name' => 'StatusApi#publishStatus', 'url' => '/api/v1/statuses', 'verb' => 'POST'],
['name' => 'StatusApi#getStatus', 'url' => '/api/v1/statuses/{id}', 'verb' => 'GET'],
['name' => 'StatusApi#deleteStatus', 'url' => '/api/v1/statuses/{id}', 'verb' => 'DELETE'],
['name' => 'StatusApi#contextStatus', 'url' => '/api/v1/statuses/{id}/context', 'verb' => 'GET'],
['name' => 'StatusApi#reblogedBy', 'url' => '/api/v1/statuses/{id}/reblogged_by', 'verb' => 'GET'],
// Api for local front-end
// TODO: front-end should be using the new ApiController

Wyświetl plik

@ -118,7 +118,7 @@ export default {
AppContent,
AppNavigation,
AppNavigationItem,
Search,
Search
},
mixins: [currentuserMixin],
data: function() {

Wyświetl plik

@ -25,12 +25,12 @@
<div class="new-post" data-id="">
<input id="file-upload"
ref="fileUploadInput"
@change="handleFileChange($event)"
multiple
type="file"
tabindex="-1"
aria-hidden="true"
class="hidden-visually">
class="hidden-visually"
@change="handleFileChange($event)">
<div class="new-post-author">
<avatar :user="currentUser.uid" :display-name="currentUser.displayName" :disable-tooltip="true"
:size="32" />
@ -62,13 +62,13 @@
@tribute-replaced="updatePostFromTribute" />
</vue-tribute>
<PreviewGrid :uploading="false" :uploadProgress="0.4" :miniatures="previewUrls" />
<PreviewGrid :uploading="false" :upload-progress="0.4" :miniatures="previewUrls" />
<div class="options">
<Button type="tertiary"
@click.prevent="clickImportInput"
<Button v-tooltip="t('social', 'Add attachment')"
type="tertiary"
:aria-label="t('social', 'Add attachment')"
v-tooltip="t('social', 'Add attachment')">
@click.prevent="clickImportInput">
<template #icon>
<FileUpload :size="22" decorative title="" />
</template>
@ -78,10 +78,10 @@
<EmojiPicker ref="emojiPicker" :search="search" :close-on-select="false"
:container="container"
@select="insert">
<Button type="tertiary"
<Button v-tooltip="t('social', 'Add emoji')"
type="tertiary"
:aria-haspopup="true"
:aria-label="t('social', 'Add emoji')"
v-tooltip="t('social', 'Add emoji')">
:aria-label="t('social', 'Add emoji')">
<template #icon>
<EmoticonOutline :size="22" decorative title="" />
</template>
@ -90,10 +90,10 @@
</div>
<div v-click-outside="hidePopoverMenu" class="popovermenu-parent">
<Button type="tertiary"
:class="currentVisibilityIconClass"
@click.prevent="togglePopoverMenu"
v-tooltip="t('social', 'Visibility')" />
<Button v-tooltip="t('social', 'Visibility')"
type="tertiary"
:class="currentVisibilityIconClass"
@click.prevent="togglePopoverMenu" />
<div :class="{open: menuOpened}" class="popovermenu">
<popover-menu :menu="visibilityPopover" />
</div>
@ -142,10 +142,10 @@ export default {
EmoticonOutline,
Button,
Send,
PreviewGrid,
PreviewGrid
},
directives: {
FocusOnCreate,
FocusOnCreate
},
mixins: [CurrentUserMixin],
props: {},
@ -256,13 +256,13 @@ export default {
computed: {
postTo() {
switch (this.type) {
case 'public':
case 'unlisted':
return t('social', 'Post')
case 'followers':
return t('social', 'Post to followers')
case 'direct':
return t('social', 'Post to mentioned users')
case 'public':
case 'unlisted':
return t('social', 'Post')
case 'followers':
return t('social', 'Post to followers')
case 'direct':
return t('social', 'Post to mentioned users')
}
},
currentVisibilityIconClass() {
@ -288,6 +288,14 @@ export default {
currentVisibilityPostLabel() {
return this.visibilityPostLabel(this.type)
},
message: {
get() {
return this.$store.state.obj.message
},
set(value) {
this.$store.commit('updateStatus', value)
}
},
visibilityPostLabel() {
return (type) => {
if (typeof type === 'undefined') {
@ -362,7 +370,7 @@ export default {
},
canPost() {
if (this.previewUrls.length > 0) {
return true;
return true
}
return this.post.length !== 0 && this.post !== '<br>'
}
@ -407,13 +415,17 @@ export default {
this.menuOpened = false
localStorage.setItem('social.lastPostType', type)
},
getPostData() {
keyup(event) {
if (event.shiftKey || event.ctrlKey) {
this.createPost(event)
}
},
updatePostFromTribute(event) {
// Trick to let vue-contenteditable know that tribute replaced a mention or hashtag
this.$refs.composerInput.oninput(event)
},
createPost: async function(event) {
let element = this.$refs.composerInput.cloneNode(true)
Array.from(element.getElementsByClassName('emoji')).forEach((emoji) => {
var em = document.createTextNode(emoji.getAttribute('alt'))
emoji.replaceWith(em)
})
let contentHtml = element.innerHTML
// Extract mentions from content and create an array out of them
@ -427,67 +439,26 @@ export default {
}
} while (match)
// Add author of original post in case of reply
if (this.replyTo !== null) {
to.push(this.replyTo.actor_info.account)
}
// Extract hashtags from content and create an array ot of them
const hashtagRegex = />#([^<]+)</g
let hashtags = []
match = null
do {
match = hashtagRegex.exec(contentHtml)
if (match) {
hashtags.push(match[1])
}
} while (match)
// Remove all html tags but </div> (wich we turn in newlines) and decode the remaining html entities
let content = contentHtml.replace(/<(?!\/div)[^>]+>/gi, '').replace(/<\/div>/gi, '\n').trim()
content = he.decode(content)
let data = {
content: content,
to: to,
hashtags: hashtags,
type: this.type,
attachments: this.previewUrls.map(preview => preview.result), // TODO send the summary and other props too
}
if (this.replyTo) {
data.replyTo = this.replyTo.id
}
return data
},
keyup(event) {
if (event.shiftKey || event.ctrlKey) {
this.createPost(event)
}
},
updatePostFromTribute(event) {
// Trick to let vue-contenteditable know that tribute replaced a mention or hashtag
this.$refs.composerInput.oninput(event)
},
createPost: async function(event) {
let postData = this.getPostData()
// Trick to validate last mention when the user directly clicks on the "post" button without validating it.
let regex = /@([-\w]+)$/
let lastMention = postData.content.match(regex)
if (lastMention) {
// Ask the server for matching accounts, and wait for the results
let result = await this.remoteSearchAccounts(lastMention[1])
// Validate the last mention only when it matches a single account
if (result.data.result.accounts.length === 1) {
postData.content = postData.content.replace(regex, '@' + result.data.result.accounts[0].account)
postData.to.push(result.data.result.accounts[0].account)
}
}
console.debug(content)
this.$store.dispatch('postStatus', content)
//
// // Trick to validate last mention when the user directly clicks on the "post" button without validating it.
// let regex = /@([-\w]+)$/
// let lastMention = postData.content.match(regex)
// if (lastMention) {
//
// // Ask the server for matching accounts, and wait for the results
// let result = await this.remoteSearchAccounts(lastMention[1])
//
// // Validate the last mention only when it matches a single account
// if (result.data.result.accounts.length === 1) {
// postData.content = postData.content.replace(regex, '@' + result.data.result.accounts[0].account)
// postData.to.push(result.data.result.accounts[0].account)
// }
// }
// Abort if the post is a direct message and no valid mentions were found
// if (this.type === 'direct' && postData.to.length === 0) {
@ -495,17 +466,6 @@ export default {
// return
// }
// Post message
this.loading = true
this.$store.dispatch('post', postData).then((response) => {
this.loading = false
this.replyTo = null
this.post = ''
this.$refs.composerInput.innerText = this.post
this.previewUrls = []
this.$store.dispatch('refreshTimeline')
})
},
closeReply() {
this.replyTo = null

Wyświetl plik

@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-or-later
<template>
<div class="upload-form">
<div class="upload-progress" v-if="false">
<div v-if="false" class="upload-progress">
<div class="upload-progress__icon">
<FileUpload :size="32" />
</div>
@ -19,7 +19,8 @@ SPDX-License-Identifier: AGPL-3.0-or-later
</div>
</div>
<div class="preview-grid">
<PreviewGridItem v-for="(item, index) in draft.attachements" :key="index" :preview="item" :index="index" />
<PreviewGridItem v-for="(item, index) in draft.attachements" :key="index" :preview="item"
:index="index" />
</div>
</div>
</template>
@ -33,27 +34,27 @@ export default {
name: 'PreviewGrid',
components: {
PreviewGridItem,
FileUpload,
FileUpload
},
computed: {
...mapState({
'draft': state => state.timeline.draft,
}),
'draft': state => state.timeline.draft
})
},
props: {
uploadProgress: {
type: Number,
required: true,
required: true
},
uploading: {
type: Boolean,
required: true,
required: true
},
miniatures: {
type: Array,
required: true,
},
},
required: true
}
}
}
</script>

Wyświetl plik

@ -4,30 +4,31 @@
<div class="preview-item__actions">
<Button type="tertiary-no-background" @click="deletePreview">
<template #icon>
<Close :size="16" fillColor="white" />
<Close :size="16" fill-color="white" />
</template>
<span>{{ t('social', 'Delete') }}</span>
</Button>
<Button type="tertiary-no-background" @click="showModal">
<template #icon>
<Edit :size="16" fillColor="white" />
<Edit :size="16" fill-color="white" />
</template>
<span>{{ t('social', 'Edit') }}</span>
</Button>
</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') }}
</div>
<Modal v-if="modal" @close="closeModal" size="small">
<Modal v-if="modal" size="small" @close="closeModal">
<div class="modal__content">
<label :for="`image-description-${index}`">
{{ t('social', 'Describe for the visually impaired') }}
</label>
<textarea :id="`image-description-${index}`" v-model="internalDescription">
</textarea>
<Button type="primary" @click="closeModal">{{ t('social', 'Close') }}</Button>
<textarea :id="`image-description-${index}`" v-model="internalDescription" />
<Button type="primary" @click="closeModal">
{{ t('social', 'Close') }}
</Button>
</div>
</Modal>
</div>
@ -46,12 +47,29 @@ export default {
Close,
Edit,
Button,
Modal,
Modal
},
props: {
preview: {
type: Object,
required: true
},
index: {
type: Number,
required: true
}
},
data() {
return {
modal: false,
internalDescription: '',
internalDescription: ''
}
},
computed: {
backgroundStyle() {
return {
backgroundImage: `url("${this.preview.preview_url}")`
}
}
},
mounted() {
@ -60,7 +78,7 @@ export default {
methods: {
deletePreview() {
this.$store.dispatch('deleteAttachement', {
id: this.preview.id,
id: this.preview.id
})
},
showModal() {
@ -70,27 +88,10 @@ export default {
this.modal = false
this.$store.dispatch('updateAttachement', {
id: this.preview.id,
description: this.internalDescription,
description: this.internalDescription
})
}
},
props: {
preview: {
type: Object,
required: true,
},
index: {
type: Number,
required: true,
},
},
computed: {
backgroundStyle() {
return {
backgroundImage: `url("${this.preview.preview_url}")`,
}
},
},
}
}
</script>

Wyświetl plik

@ -19,13 +19,13 @@ import Avatar from '@nextcloud/vue/dist/Components/Avatar'
export default {
name: 'TimelineAvatar',
components: {
Avatar,
Avatar
},
props: {
item: {
type: Object,
default: () => {},
},
default: () => {}
}
},
computed: {
userTest() {
@ -33,8 +33,8 @@ export default {
},
avatarUrl() {
return OC.generateUrl('/apps/social/api/v1/global/actor/avatar?id=' + this.item.attributedTo)
},
},
}
}
}
</script>

Wyświetl plik

@ -33,31 +33,31 @@
<post-attachment :attachments="item.attachment" />
</div>
<div v-if="this.$route.params.type !== 'notifications' && !serverData.public" class="post-actions">
<Button type="tertiary-no-background"
v-tooltip="t('social', 'Reply')"
<Button v-tooltip="t('social', 'Reply')"
type="tertiary-no-background"
@click="reply">
<template #icon>
<Reply :size="20" />
</template>
</Button>
<Button type="tertiary-no-background"
v-tooltip="t('social', 'Boost')"
<Button v-tooltip="t('social', 'Boost')"
type="tertiary-no-background"
@click="boost">
<template #icon>
<Repeat :size="20" :fill-color="isBoosted ? 'blue' : 'black'" />
</template>
</Button>
<Button v-if="!isLiked"
type="tertiary-no-background"
v-tooltip="t('social', 'Like')"
type="tertiary-no-background"
@click="like">
<template #icon>
<HeartOutline :size="20" />
</template>
</Button>
<Button v-if="isLiked"
type="tertiary-no-background"
v-tooltip="t('social', 'Undo Like')"
type="tertiary-no-background"
@click="like">
<template #icon>
<Heart :size="20" :fill-color="'var(--color-error)'" />
@ -65,8 +65,8 @@
</Button>
<Actions>
<ActionButton v-if="item.actor_info.account === cloudId"
@click="remove()"
icon="icon-delete">
icon="icon-delete"
@click="remove()">
{{ t('social', 'Delete') }}
</ActionButton>
</Actions>
@ -105,7 +105,7 @@ export default {
Repeat,
Reply,
Heart,
HeartOutline,
HeartOutline
},
mixins: [currentUser],
props: {

Wyświetl plik

@ -22,5 +22,5 @@ Vue.prototype.OCA = OCA
/* eslint-disable-next-line no-new */
new Vue({
render: h => h(App),
render: h => h(App)
}).$mount('#settings-personal')

Wyświetl plik

@ -0,0 +1,79 @@
// SPDX-FileCopyrightText: 2022 Carl Schwan <carl@carlschwan.eu>
// SPDX-License-Identifier: AGPL-3.0-or-later
const state = {
attachements: [],
status: '',
sensitive: false
}
const mutations = {
addAttachement(state, { id, description, url, preview_url }) {
state.attachements.push({ id, description, url, preview_url })
},
updateAttachement(state, { id, description, url, preview_url }) {
const index = state.attachements.findIndex(item => {
return id === item.id
})
state.attachements.splice(index, 1, { id, description, url, preview_url })
},
deleteAttachement(state, { id }) {
const index = state.attachements.findIndex(item => {
return id === item.id
})
state.attachements.splice(index, 1)
},
clearAttachements(state) {
state.attachements.splice(0)
},
updateSensitive(sensitive, status) {
state.sensitive = sensitive
}
}
const actions = {
async uploadAttachement(context, formData) {
const res = await axios.post(generateUrl('apps/social/api/v1/media'), formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
})
context.commit('addAttachement', {
id: res.data.id,
description: res.data.description,
url: res.data.url,
preview_url: res.data.preview_url
})
},
async updateAttachement(context, { id, description }) {
const res = await axios.put(generateUrl('apps/social/api/v1/media/' + id), {
description
})
context.commit('updateAttachement', {
id: res.data.id,
description: res.data.description,
url: res.data.url,
preview_url: res.data.preview_url
})
},
async deleteAttachement(context, { id }) {
const res = await axios.delete(generateUrl('apps/social/api/v1/media/' + id))
context.commit('deleteAttachement', {
id: res.data.id
})
},
async postStatus({ commit, state }, text) {
const data = {
status: text,
media_ids: state.attachements.map(attachement => attachement.id),
sensitive: state.sensitive
}
try {
const response = await axios.post(generateUrl('apps/social/api/v1/statuses'), data)
} catch (error) {
OC.Notification.showTemporary('Failed to create a post')
Logger.error('Failed to create a post', { 'error': error.response })
}
commit('clearAttachements')
}
}

Wyświetl plik

@ -26,6 +26,7 @@ import Vuex from 'vuex'
import timeline from './timeline'
import account from './account'
import settings from './settings'
import composer from './composer'
Vue.use(Vuex)
@ -35,7 +36,8 @@ export default new Vuex.Store({
modules: {
timeline,
account,
settings
settings,
composer
},
strict: debug
})

Wyświetl plik

@ -55,7 +55,7 @@ const state = {
composerDisplayStatus: false,
draft: {
attachements: []
},
}
}
const mutations = {
addToTimeline(state, data) {
@ -115,21 +115,24 @@ const mutations = {
Vue.set(state.timeline[parentAnnounce.id].cache[parentAnnounce.object].object.action.values, 'boosted', false)
}
},
addAttachement(state, {id, description, url, preview_url}) {
state.draft.attachements.push({id, description, url, preview_url})
addAttachement(state, { id, description, url, preview_url }) {
state.draft.attachements.push({ id, description, url, preview_url })
},
updateAttachement(state, {id, description, url, preview_url}) {
updateAttachement(state, { id, description, url, preview_url }) {
const index = state.draft.attachements.findIndex(item => {
return id === item.id
})
state.draft.attachements.splice(index, 1, {id, description, url, preview_url})
state.draft.attachements.splice(index, 1, { id, description, url, preview_url })
},
deleteAttachement(state, {id}) {
deleteAttachement(state, { id }) {
const index = state.draft.attachements.findIndex(item => {
return id === item.id
})
state.draft.attachements.splice(index, 1)
},
clearAttachements(state) {
state.draft.attachements.splice(0)
}
}
const getters = {
getComposerDisplayStatus(state) {
@ -166,43 +169,44 @@ const actions = {
const res = await axios.post(generateUrl('apps/social/api/v1/media'), formData, {
headers: {
'Content-Type': 'multipart/form-data'
},
}
})
context.commit('addAttachement', {
id: res.data.id,
description: res.data.description,
url: res.data.url,
preview_url: res.data.preview_url,
preview_url: res.data.preview_url
})
},
async updateAttachement(context, {id, description}) {
async updateAttachement(context, { id, description }) {
const res = await axios.put(generateUrl('apps/social/api/v1/media/' + id), {
description,
description
})
context.commit('updateAttachement', {
id: res.data.id,
description: res.data.description,
url: res.data.url,
preview_url: res.data.preview_url,
preview_url: res.data.preview_url
})
},
async deleteAttachement(context, {id}) {
async deleteAttachement(context, { id }) {
const res = await axios.delete(generateUrl('apps/social/api/v1/media/' + id))
context.commit('deleteAttachement', {
id: res.data.id,
id: res.data.id
})
},
post(context, post) {
return new Promise((resolve, reject) => {
axios.post(generateUrl('apps/social/api/v1/post'), { data: post }).then((response) => {
Logger.info('Post created with token ' + response.data.result.token)
resolve(response)
}).catch((error) => {
OC.Notification.showTemporary('Failed to create a post')
Logger.error('Failed to create a post', { 'error': error.response })
reject(error)
})
})
async postStatus({ commit, state }, text) {
const data = {
status: text,
media_ids: state.draft.attachements.map(attachement => attachement.id)
}
try {
const response = axios.post(generateUrl('apps/social/api/v1/statuses'), data)
} catch (error) {
OC.Notification.showTemporary('Failed to create a post')
Logger.error('Failed to create a post', { 'error': error.response })
}
commit('clearAttachements')
},
postDelete(context, post) {
return axios.delete(generateUrl(`apps/social/api/v1/post?id=${post.id}`)).then((response) => {

Wyświetl plik

@ -51,7 +51,7 @@ export default {
name: 'SetupUser',
components: {
CheckboxRadioSwitch,
SettingsSection,
SettingsSection
},
data() {
return {