diff options
author | Carl Schwan <carl@carlschwan.eu> | 2022-09-12 13:43:13 +0200 |
---|---|---|
committer | Carl Schwan <carl@carlschwan.eu> | 2022-09-12 13:43:13 +0200 |
commit | ee1b2945bb10cf56f33a9ae17126808a18d9e561 (patch) | |
tree | 0c4434ba88a7ba963cefe557f1b7ce1981226614 | |
parent | a2fca565a114236cf2008fe233a5cd5289e2c7df (diff) |
More stuff to commitdb-rewrite
Signed-off-by: Carl Schwan <carl@carlschwan.eu>
-rw-r--r-- | appinfo/routes.php | 8 | ||||
-rw-r--r-- | src/App.vue | 2 | ||||
-rw-r--r-- | src/components/Composer/Composer.vue | 156 | ||||
-rw-r--r-- | src/components/Composer/PreviewGrid.vue | 21 | ||||
-rw-r--r-- | src/components/Composer/PreviewGridItem.vue | 59 | ||||
-rw-r--r-- | src/components/TimelineAvatar.vue | 10 | ||||
-rw-r--r-- | src/components/TimelinePost.vue | 18 | ||||
-rw-r--r-- | src/settings-personal.js | 2 | ||||
-rw-r--r-- | src/store/composer.js | 79 | ||||
-rw-r--r-- | src/store/index.js | 4 | ||||
-rw-r--r-- | src/store/timeline.js | 52 | ||||
-rw-r--r-- | src/views/SettingsPersonal.vue | 2 |
12 files changed, 233 insertions, 180 deletions
diff --git a/appinfo/routes.php b/appinfo/routes.php index b845fa82..3ef5ea91 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -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 diff --git a/src/App.vue b/src/App.vue index b9f4a5b3..377c7af6 100644 --- a/src/App.vue +++ b/src/App.vue @@ -118,7 +118,7 @@ export default { AppContent, AppNavigation, AppNavigationItem, - Search, + Search }, mixins: [currentuserMixin], data: function() { diff --git a/src/components/Composer/Composer.vue b/src/components/Composer/Composer.vue index 543c95d9..31121762 100644 --- a/src/components/Composer/Composer.vue +++ b/src/components/Composer/Composer.vue @@ -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 diff --git a/src/components/Composer/PreviewGrid.vue b/src/components/Composer/PreviewGrid.vue index 6b3f03a2..c218e306 100644 --- a/src/components/Composer/PreviewGrid.vue +++ b/src/components/Composer/PreviewGrid.vue @@ -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> diff --git a/src/components/Composer/PreviewGridItem.vue b/src/components/Composer/PreviewGridItem.vue index 1e2d922a..48f8fbaa 100644 --- a/src/components/Composer/PreviewGridItem.vue +++ b/src/components/Composer/PreviewGridItem.vue @@ -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> diff --git a/src/components/TimelineAvatar.vue b/src/components/TimelineAvatar.vue index 70938266..9a730175 100644 --- a/src/components/TimelineAvatar.vue +++ b/src/components/TimelineAvatar.vue @@ -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> diff --git a/src/components/TimelinePost.vue b/src/components/TimelinePost.vue index 764e8c02..75f61992 100644 --- a/src/components/TimelinePost.vue +++ b/src/components/TimelinePost.vue @@ -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: { diff --git a/src/settings-personal.js b/src/settings-personal.js index 2359e242..07722c65 100644 --- a/src/settings-personal.js +++ b/src/settings-personal.js @@ -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') diff --git a/src/store/composer.js b/src/store/composer.js new file mode 100644 index 00000000..62e9b75b --- /dev/null +++ b/src/store/composer.js @@ -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') + } +} diff --git a/src/store/index.js b/src/store/index.js index 4076bff7..34bd54e2 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -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 }) diff --git a/src/store/timeline.js b/src/store/timeline.js index 9f8c0d49..741fc2a4 100644 --- a/src/store/timeline.js +++ b/src/store/timeline.js @@ -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) => { diff --git a/src/views/SettingsPersonal.vue b/src/views/SettingsPersonal.vue index fea163ea..07209a93 100644 --- a/src/views/SettingsPersonal.vue +++ b/src/views/SettingsPersonal.vue @@ -51,7 +51,7 @@ export default { name: 'SetupUser', components: { CheckboxRadioSwitch, - SettingsSection, + SettingsSection }, data() { return { |