Compare commits
25 Commits
develop
...
tusooa/sav
Author | SHA1 | Date |
---|---|---|
tusooa | e5382781a4 | |
tusooa | 81cfcae433 | |
tusooa | a73f9771ee | |
tusooa | 7ec3f49c3e | |
tusooa | c9cacc32a4 | |
tusooa | f2bdc1c563 | |
tusooa | aa59adc81f | |
tusooa | b3236e707c | |
tusooa | bae81f8d39 | |
tusooa | d884082761 | |
tusooa | 3c39269be8 | |
tusooa | 5e08c4537b | |
tusooa | 0256ff5f63 | |
tusooa | 39b42da724 | |
tusooa | 19b5bb06a0 | |
tusooa | 3c828f3575 | |
tusooa | 5cddf18a4e | |
tusooa | 4a044d067a | |
tusooa | 050266ee39 | |
tusooa | 8d27c68d5f | |
tusooa | a9f374b18f | |
tusooa | f13444a3c8 | |
tusooa | dcd4587525 | |
tusooa | d4444cd0b1 | |
tusooa | 4daa272fdf |
|
@ -703,6 +703,13 @@ option {
|
||||||
color: white;
|
color: white;
|
||||||
color: var(--badgeNotificationText, white);
|
color: var(--badgeNotificationText, white);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.badge-neutral {
|
||||||
|
background-color: $fallback--cGreen;
|
||||||
|
background-color: var(--badgeNeutral, $fallback--cGreen);
|
||||||
|
color: white;
|
||||||
|
color: var(--badgeNeutralText, white);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.alert {
|
.alert {
|
||||||
|
|
|
@ -377,6 +377,8 @@ const afterStoreSetup = async ({ store, i18n }) => {
|
||||||
getInstanceConfig({ store })
|
getInstanceConfig({ store })
|
||||||
])
|
])
|
||||||
|
|
||||||
|
await store.dispatch('loadDrafts')
|
||||||
|
|
||||||
// Start fetching things that don't need to block the UI
|
// Start fetching things that don't need to block the UI
|
||||||
store.dispatch('fetchMutes')
|
store.dispatch('fetchMutes')
|
||||||
store.dispatch('startFetchingAnnouncements')
|
store.dispatch('startFetchingAnnouncements')
|
||||||
|
|
|
@ -25,6 +25,7 @@ import ListsTimeline from 'components/lists_timeline/lists_timeline.vue'
|
||||||
import ListsEdit from 'components/lists_edit/lists_edit.vue'
|
import ListsEdit from 'components/lists_edit/lists_edit.vue'
|
||||||
import NavPanel from 'src/components/nav_panel/nav_panel.vue'
|
import NavPanel from 'src/components/nav_panel/nav_panel.vue'
|
||||||
import AnnouncementsPage from 'components/announcements_page/announcements_page.vue'
|
import AnnouncementsPage from 'components/announcements_page/announcements_page.vue'
|
||||||
|
import Drafts from 'components/drafts/drafts.vue'
|
||||||
|
|
||||||
export default (store) => {
|
export default (store) => {
|
||||||
const validateAuthenticatedRoute = (to, from, next) => {
|
const validateAuthenticatedRoute = (to, from, next) => {
|
||||||
|
@ -78,6 +79,7 @@ export default (store) => {
|
||||||
{ name: 'who-to-follow', path: '/who-to-follow', component: WhoToFollow, beforeEnter: validateAuthenticatedRoute },
|
{ name: 'who-to-follow', path: '/who-to-follow', component: WhoToFollow, beforeEnter: validateAuthenticatedRoute },
|
||||||
{ name: 'about', path: '/about', component: About },
|
{ name: 'about', path: '/about', component: About },
|
||||||
{ name: 'announcements', path: '/announcements', component: AnnouncementsPage },
|
{ name: 'announcements', path: '/announcements', component: AnnouncementsPage },
|
||||||
|
{ name: 'drafts', path: '/drafts', component: Drafts },
|
||||||
{ name: 'user-profile', path: '/users/:name', component: UserProfile },
|
{ name: 'user-profile', path: '/users/:name', component: UserProfile },
|
||||||
{ name: 'legacy-user-profile', path: '/:name', component: UserProfile },
|
{ name: 'legacy-user-profile', path: '/:name', component: UserProfile },
|
||||||
{ name: 'lists', path: '/lists', component: Lists },
|
{ name: 'lists', path: '/lists', component: Lists },
|
||||||
|
|
|
@ -0,0 +1,64 @@
|
||||||
|
import PostStatusForm from 'src/components/post_status_form/post_status_form.vue'
|
||||||
|
import EditStatusForm from 'src/components/edit_status_form/edit_status_form.vue'
|
||||||
|
import ConfirmModal from 'src/components/confirm_modal/confirm_modal.vue'
|
||||||
|
import StatusContent from 'src/components/status_content/status_content.vue'
|
||||||
|
|
||||||
|
const Draft = {
|
||||||
|
components: {
|
||||||
|
PostStatusForm,
|
||||||
|
EditStatusForm,
|
||||||
|
ConfirmModal,
|
||||||
|
StatusContent
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
draft: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
editing: false,
|
||||||
|
showingConfirmDialog: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
relAttrs () {
|
||||||
|
if (this.draft.type === 'edit') {
|
||||||
|
return { statusId: this.draft.refId }
|
||||||
|
} else if (this.draft.type === 'reply') {
|
||||||
|
return { replyTo: this.draft.refId }
|
||||||
|
} else {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
postStatusFormProps () {
|
||||||
|
return {
|
||||||
|
draftId: this.draft.id,
|
||||||
|
...this.relAttrs
|
||||||
|
}
|
||||||
|
},
|
||||||
|
refStatus () {
|
||||||
|
return this.draft.refId ? this.$store.state.statuses.allStatusesObject[this.draft.refId] : undefined
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
toggleEditing () {
|
||||||
|
this.editing = !this.editing
|
||||||
|
},
|
||||||
|
abandon () {
|
||||||
|
this.showingConfirmDialog = true
|
||||||
|
},
|
||||||
|
doAbandon () {
|
||||||
|
this.$store.dispatch('abandonDraft', { id: this.draft.id })
|
||||||
|
.then(() => {
|
||||||
|
this.hideConfirmDialog()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
hideConfirmDialog () {
|
||||||
|
this.showingConfirmDialog = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Draft
|
|
@ -0,0 +1,104 @@
|
||||||
|
<template>
|
||||||
|
<article class="Draft">
|
||||||
|
<div class="actions">
|
||||||
|
<button
|
||||||
|
class="btn button-default"
|
||||||
|
:class="{ toggled: editing }"
|
||||||
|
:aria-expanded="editing"
|
||||||
|
@click.prevent.stop="toggleEditing"
|
||||||
|
>
|
||||||
|
{{ $t('drafts.continue') }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="btn button-default"
|
||||||
|
@click.prevent.stop="abandon"
|
||||||
|
>
|
||||||
|
{{ $t('drafts.abandon') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="!editing"
|
||||||
|
class="status-content"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<i18n-t
|
||||||
|
v-if="draft.type === 'reply' || draft.type === 'edit'"
|
||||||
|
tag="span"
|
||||||
|
:keypath="draft.type === 'reply' ? 'drafts.replying' : 'drafts.editing'"
|
||||||
|
>
|
||||||
|
<template #statusLink>
|
||||||
|
<router-link
|
||||||
|
class="faint-link"
|
||||||
|
:to="{ name: 'conversation', params: { id: draft.refId } }"
|
||||||
|
>
|
||||||
|
{{ refStatus ? refStatus.external_url : $t('drafts.unavailable') }}
|
||||||
|
</router-link>
|
||||||
|
</template>
|
||||||
|
</i18n-t>
|
||||||
|
<StatusContent
|
||||||
|
v-if="draft.refId && refStatus"
|
||||||
|
class="status-content"
|
||||||
|
:status="refStatus"
|
||||||
|
:compact="true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p>{{ draft.status }}</p>
|
||||||
|
</div>
|
||||||
|
<div v-if="editing">
|
||||||
|
<PostStatusForm
|
||||||
|
v-if="draft.type !== 'edit'"
|
||||||
|
v-bind="postStatusFormProps"
|
||||||
|
/>
|
||||||
|
<EditStatusForm
|
||||||
|
v-else
|
||||||
|
:params="postStatusFormProps"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<teleport to="#modal">
|
||||||
|
<confirm-modal
|
||||||
|
v-if="showingConfirmDialog"
|
||||||
|
:title="$t('drafts.abandon_confirm_title')"
|
||||||
|
:confirm-text="$t('drafts.abandon_confirm_accept_button')"
|
||||||
|
:cancel-text="$t('drafts.abandon_confirm_cancel_button')"
|
||||||
|
@accepted="doAbandon"
|
||||||
|
@cancelled="hideConfirmDialog"
|
||||||
|
>
|
||||||
|
{{ $t('drafts.abandon_confirm') }}
|
||||||
|
</confirm-modal>
|
||||||
|
</teleport>
|
||||||
|
</article>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./draft.js"></script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import "src/variables";
|
||||||
|
|
||||||
|
.Draft {
|
||||||
|
margin: 1em;
|
||||||
|
|
||||||
|
.status-content {
|
||||||
|
border: 1px solid $fallback--faint;
|
||||||
|
border-color: var(--faint, $fallback--faint);
|
||||||
|
border-radius: $fallback--inputRadius;
|
||||||
|
border-radius: var(--inputRadius, $fallback--inputRadius);
|
||||||
|
color: $fallback--text;
|
||||||
|
color: var(--text, $fallback--text);
|
||||||
|
padding: 0.5em;
|
||||||
|
margin: 0.5em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-evenly;
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
flex: 1;
|
||||||
|
margin-left: 1em;
|
||||||
|
margin-right: 1em;
|
||||||
|
max-width: 10em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,52 @@
|
||||||
|
import DialogModal from 'src/components/dialog_modal/dialog_modal.vue'
|
||||||
|
|
||||||
|
const DraftCloser = {
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
showing: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
DialogModal
|
||||||
|
},
|
||||||
|
emits: [
|
||||||
|
'save',
|
||||||
|
'discard'
|
||||||
|
],
|
||||||
|
computed: {
|
||||||
|
action () {
|
||||||
|
if (this.$store.getters.mergedConfig.autoSaveDraft) {
|
||||||
|
return 'save'
|
||||||
|
} else {
|
||||||
|
return this.$store.getters.mergedConfig.unsavedPostAction
|
||||||
|
}
|
||||||
|
},
|
||||||
|
shouldConfirm () {
|
||||||
|
return this.action === 'confirm'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
requestClose () {
|
||||||
|
if (this.shouldConfirm) {
|
||||||
|
this.showing = true
|
||||||
|
} else if (this.action === 'save') {
|
||||||
|
this.save()
|
||||||
|
} else {
|
||||||
|
this.discard()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
save () {
|
||||||
|
this.$emit('save')
|
||||||
|
this.showing = false
|
||||||
|
},
|
||||||
|
discard () {
|
||||||
|
this.$emit('discard')
|
||||||
|
this.showing = false
|
||||||
|
},
|
||||||
|
cancel () {
|
||||||
|
this.showing = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DraftCloser
|
|
@ -0,0 +1,43 @@
|
||||||
|
<template>
|
||||||
|
<teleport to="#modal">
|
||||||
|
<dialog-modal
|
||||||
|
v-if="showing"
|
||||||
|
v-body-scroll-lock="true"
|
||||||
|
class="confirm-modal"
|
||||||
|
:on-cancel="cancel"
|
||||||
|
>
|
||||||
|
<template #header>
|
||||||
|
<span>
|
||||||
|
{{ $t('post_status.close_confirm_title') }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
{{ $t('post_status.close_confirm') }}
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<button
|
||||||
|
class="btn button-default"
|
||||||
|
@click.prevent="save"
|
||||||
|
>
|
||||||
|
{{ $t('post_status.close_confirm_save_button') }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="btn button-default"
|
||||||
|
@click.prevent="discard"
|
||||||
|
>
|
||||||
|
{{ $t('post_status.close_confirm_discard_button') }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="btn button-default"
|
||||||
|
@click.prevent="cancel"
|
||||||
|
>
|
||||||
|
{{ $t('post_status.close_confirm_continue_composing_button') }}
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
</dialog-modal>
|
||||||
|
</teleport>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./draft_closer.js"></script>
|
|
@ -0,0 +1,16 @@
|
||||||
|
import Draft from 'src/components/draft/draft.vue'
|
||||||
|
import List from 'src/components/list/list.vue'
|
||||||
|
|
||||||
|
const Drafts = {
|
||||||
|
components: {
|
||||||
|
Draft,
|
||||||
|
List
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
drafts () {
|
||||||
|
return this.$store.getters.draftsArray
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Drafts
|
|
@ -0,0 +1,24 @@
|
||||||
|
<template>
|
||||||
|
<div class="Drafts">
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<div class="title">
|
||||||
|
{{ $t('drafts.drafts') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<List
|
||||||
|
:items="drafts"
|
||||||
|
>
|
||||||
|
<template #item="{ item: draft }">
|
||||||
|
<Draft
|
||||||
|
:draft="draft"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</List>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./drafts.js"></script>
|
|
@ -0,0 +1,44 @@
|
||||||
|
import PostStatusForm from '../post_status_form/post_status_form.vue'
|
||||||
|
import statusPosterService from '../../services/status_poster/status_poster.service.js'
|
||||||
|
|
||||||
|
const EditStatusForm = {
|
||||||
|
components: {
|
||||||
|
PostStatusForm
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
params: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
requestClose () {
|
||||||
|
this.$refs.postStatusForm.requestClose()
|
||||||
|
},
|
||||||
|
doEditStatus ({ status, spoilerText, sensitive, media, contentType, poll }) {
|
||||||
|
const params = {
|
||||||
|
store: this.$store,
|
||||||
|
statusId: this.params.statusId,
|
||||||
|
status,
|
||||||
|
spoilerText,
|
||||||
|
sensitive,
|
||||||
|
poll,
|
||||||
|
media,
|
||||||
|
contentType
|
||||||
|
}
|
||||||
|
|
||||||
|
return statusPosterService.editStatus(params)
|
||||||
|
.then((data) => {
|
||||||
|
return data
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error('Error editing status', err)
|
||||||
|
return {
|
||||||
|
error: err.message
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EditStatusForm
|
|
@ -0,0 +1,11 @@
|
||||||
|
<template>
|
||||||
|
<PostStatusForm
|
||||||
|
ref="postStatusForm"
|
||||||
|
v-bind="params"
|
||||||
|
:post-handler="doEditStatus"
|
||||||
|
:disable-polls="true"
|
||||||
|
:disable-visibility-selector="true"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./edit_status_form.js"></script>
|
|
@ -1,11 +1,10 @@
|
||||||
import PostStatusForm from '../post_status_form/post_status_form.vue'
|
import EditStatusForm from '../edit_status_form/edit_status_form.vue'
|
||||||
import Modal from '../modal/modal.vue'
|
import Modal from '../modal/modal.vue'
|
||||||
import statusPosterService from '../../services/status_poster/status_poster.service.js'
|
|
||||||
import get from 'lodash/get'
|
import get from 'lodash/get'
|
||||||
|
|
||||||
const EditStatusModal = {
|
const EditStatusModal = {
|
||||||
components: {
|
components: {
|
||||||
PostStatusForm,
|
EditStatusForm,
|
||||||
Modal
|
Modal
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
|
@ -43,30 +42,10 @@ const EditStatusModal = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
doEditStatus ({ status, spoilerText, sensitive, media, contentType, poll }) {
|
|
||||||
const params = {
|
|
||||||
store: this.$store,
|
|
||||||
statusId: this.$store.state.editStatus.params.statusId,
|
|
||||||
status,
|
|
||||||
spoilerText,
|
|
||||||
sensitive,
|
|
||||||
poll,
|
|
||||||
media,
|
|
||||||
contentType
|
|
||||||
}
|
|
||||||
|
|
||||||
return statusPosterService.editStatus(params)
|
|
||||||
.then((data) => {
|
|
||||||
return data
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
console.error('Error editing status', err)
|
|
||||||
return {
|
|
||||||
error: err.message
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
closeModal () {
|
closeModal () {
|
||||||
|
this.$refs.editStatusForm.requestClose()
|
||||||
|
},
|
||||||
|
doCloseModal () {
|
||||||
this.$store.dispatch('closeEditStatusModal')
|
this.$store.dispatch('closeEditStatusModal')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,13 +8,12 @@
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
{{ $t('post_status.edit_status') }}
|
{{ $t('post_status.edit_status') }}
|
||||||
</div>
|
</div>
|
||||||
<PostStatusForm
|
<EditStatusForm
|
||||||
|
ref="editStatusForm"
|
||||||
class="panel-body"
|
class="panel-body"
|
||||||
v-bind="params"
|
:params="params"
|
||||||
:post-handler="doEditStatus"
|
@posted="doCloseModal"
|
||||||
:disable-polls="true"
|
@can-close="doCloseModal"
|
||||||
:disable-visibility-selector="true"
|
|
||||||
@posted="closeModal"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
|
@ -19,7 +19,8 @@ import {
|
||||||
faInfoCircle,
|
faInfoCircle,
|
||||||
faStream,
|
faStream,
|
||||||
faList,
|
faList,
|
||||||
faBullhorn
|
faBullhorn,
|
||||||
|
faFilePen
|
||||||
} from '@fortawesome/free-solid-svg-icons'
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
library.add(
|
library.add(
|
||||||
|
@ -34,7 +35,8 @@ library.add(
|
||||||
faInfoCircle,
|
faInfoCircle,
|
||||||
faStream,
|
faStream,
|
||||||
faList,
|
faList,
|
||||||
faBullhorn
|
faBullhorn,
|
||||||
|
faFilePen
|
||||||
)
|
)
|
||||||
const NavPanel = {
|
const NavPanel = {
|
||||||
props: ['forceExpand', 'forceEditMode'],
|
props: ['forceExpand', 'forceEditMode'],
|
||||||
|
|
|
@ -56,6 +56,7 @@ export const ROOT_ITEMS = {
|
||||||
route: 'chats',
|
route: 'chats',
|
||||||
icon: 'comments',
|
icon: 'comments',
|
||||||
label: 'nav.chats',
|
label: 'nav.chats',
|
||||||
|
badgeStyle: 'notification',
|
||||||
badgeGetter: 'unreadChatCount',
|
badgeGetter: 'unreadChatCount',
|
||||||
criteria: ['chats']
|
criteria: ['chats']
|
||||||
},
|
},
|
||||||
|
@ -63,6 +64,7 @@ export const ROOT_ITEMS = {
|
||||||
route: 'friend-requests',
|
route: 'friend-requests',
|
||||||
icon: 'user-plus',
|
icon: 'user-plus',
|
||||||
label: 'nav.friend_requests',
|
label: 'nav.friend_requests',
|
||||||
|
badgeStyle: 'notification',
|
||||||
criteria: ['lockedUser'],
|
criteria: ['lockedUser'],
|
||||||
badgeGetter: 'followRequestCount'
|
badgeGetter: 'followRequestCount'
|
||||||
},
|
},
|
||||||
|
@ -76,8 +78,16 @@ export const ROOT_ITEMS = {
|
||||||
route: 'announcements',
|
route: 'announcements',
|
||||||
icon: 'bullhorn',
|
icon: 'bullhorn',
|
||||||
label: 'nav.announcements',
|
label: 'nav.announcements',
|
||||||
|
badgeStyle: 'notification',
|
||||||
badgeGetter: 'unreadAnnouncementCount',
|
badgeGetter: 'unreadAnnouncementCount',
|
||||||
criteria: ['announcements']
|
criteria: ['announcements']
|
||||||
|
},
|
||||||
|
drafts: {
|
||||||
|
route: 'drafts',
|
||||||
|
icon: 'file-pen',
|
||||||
|
label: 'nav.drafts',
|
||||||
|
badgeStyle: 'neutral',
|
||||||
|
badgeGetter: 'draftCount'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -35,7 +35,8 @@
|
||||||
<slot />
|
<slot />
|
||||||
<div
|
<div
|
||||||
v-if="item.badgeGetter && getters[item.badgeGetter]"
|
v-if="item.badgeGetter && getters[item.badgeGetter]"
|
||||||
class="badge badge-notification"
|
class="badge"
|
||||||
|
:class="[`badge-${item.badgeStyle}`]"
|
||||||
>
|
>
|
||||||
{{ getters[item.badgeGetter] }}
|
{{ getters[item.badgeGetter] }}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -19,6 +19,7 @@
|
||||||
<div
|
<div
|
||||||
v-if="item.badgeGetter && getters[item.badgeGetter]"
|
v-if="item.badgeGetter && getters[item.badgeGetter]"
|
||||||
class="alert-dot"
|
class="alert-dot"
|
||||||
|
:class="[`alert-dot-${item.badgeStyle}`]"
|
||||||
/>
|
/>
|
||||||
</router-link>
|
</router-link>
|
||||||
</span>
|
</span>
|
||||||
|
@ -42,8 +43,16 @@
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: calc(50% - 0.75em);
|
right: calc(50% - 0.75em);
|
||||||
top: calc(50% - 0.5em);
|
top: calc(50% - 0.5em);
|
||||||
background-color: $fallback--cRed;
|
|
||||||
background-color: var(--badgeNotification, $fallback--cRed);
|
&.alert-dot-notification {
|
||||||
|
background-color: $fallback--cRed;
|
||||||
|
background-color: var(--badgeNotification, $fallback--cRed);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.alert-dot-neutral {
|
||||||
|
background-color: $fallback--cGreen;
|
||||||
|
background-color: var(--badgeNeutral, $fallback--cGreen);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.pinned-item {
|
.pinned-item {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import * as DateUtils from 'src/services/date_utils/date_utils.js'
|
import * as DateUtils from 'src/services/date_utils/date_utils.js'
|
||||||
import { uniq } from 'lodash'
|
import { pollFallback } from 'src/services/poll/poll.service.js'
|
||||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
import Select from '../select/select.vue'
|
import Select from '../select/select.vue'
|
||||||
import {
|
import {
|
||||||
|
@ -17,14 +17,33 @@ export default {
|
||||||
Select
|
Select
|
||||||
},
|
},
|
||||||
name: 'PollForm',
|
name: 'PollForm',
|
||||||
props: ['visible'],
|
props: {
|
||||||
data: () => ({
|
visible: {},
|
||||||
pollType: 'single',
|
params: {
|
||||||
options: ['', ''],
|
type: Object,
|
||||||
expiryAmount: 10,
|
required: true
|
||||||
expiryUnit: 'minutes'
|
}
|
||||||
}),
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
pollType: {
|
||||||
|
get () { return pollFallback(this.params, 'pollType') },
|
||||||
|
set (newVal) { this.params.pollType = newVal }
|
||||||
|
},
|
||||||
|
options () {
|
||||||
|
const hasOptions = !!this.params.options
|
||||||
|
if (!hasOptions) {
|
||||||
|
this.params.options = pollFallback(this.params, 'options')
|
||||||
|
}
|
||||||
|
return this.params.options
|
||||||
|
},
|
||||||
|
expiryAmount: {
|
||||||
|
get () { return pollFallback(this.params, 'expiryAmount') },
|
||||||
|
set (newVal) { this.params.expiryAmount = newVal }
|
||||||
|
},
|
||||||
|
expiryUnit: {
|
||||||
|
get () { return pollFallback(this.params, 'expiryUnit') },
|
||||||
|
set (newVal) { this.params.expiryUnit = newVal }
|
||||||
|
},
|
||||||
pollLimits () {
|
pollLimits () {
|
||||||
return this.$store.state.instance.pollLimits
|
return this.$store.state.instance.pollLimits
|
||||||
},
|
},
|
||||||
|
@ -89,7 +108,6 @@ export default {
|
||||||
deleteOption (index, event) {
|
deleteOption (index, event) {
|
||||||
if (this.options.length > 2) {
|
if (this.options.length > 2) {
|
||||||
this.options.splice(index, 1)
|
this.options.splice(index, 1)
|
||||||
this.updatePollToParent()
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
convertExpiryToUnit (unit, amount) {
|
convertExpiryToUnit (unit, amount) {
|
||||||
|
@ -104,24 +122,6 @@ export default {
|
||||||
Math.max(this.minExpirationInCurrentUnit, this.expiryAmount)
|
Math.max(this.minExpirationInCurrentUnit, this.expiryAmount)
|
||||||
this.expiryAmount =
|
this.expiryAmount =
|
||||||
Math.min(this.maxExpirationInCurrentUnit, this.expiryAmount)
|
Math.min(this.maxExpirationInCurrentUnit, this.expiryAmount)
|
||||||
this.updatePollToParent()
|
|
||||||
},
|
|
||||||
updatePollToParent () {
|
|
||||||
const expiresIn = this.convertExpiryFromUnit(
|
|
||||||
this.expiryUnit,
|
|
||||||
this.expiryAmount
|
|
||||||
)
|
|
||||||
|
|
||||||
const options = uniq(this.options.filter(option => option !== ''))
|
|
||||||
if (options.length < 2) {
|
|
||||||
this.$emit('update-poll', { error: this.$t('polls.not_enough_options') })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
this.$emit('update-poll', {
|
|
||||||
options,
|
|
||||||
multiple: this.pollType === 'multiple',
|
|
||||||
expiresIn
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,11 +9,13 @@ import StatusContent from '../status_content/status_content.vue'
|
||||||
import fileTypeService from '../../services/file_type/file_type.service.js'
|
import fileTypeService from '../../services/file_type/file_type.service.js'
|
||||||
import { findOffset } from '../../services/offset_finder/offset_finder.service.js'
|
import { findOffset } from '../../services/offset_finder/offset_finder.service.js'
|
||||||
import { propsToNative } from '../../services/attributes_helper/attributes_helper.service.js'
|
import { propsToNative } from '../../services/attributes_helper/attributes_helper.service.js'
|
||||||
|
import { pollFormToMasto } from 'src/services/poll/poll.service.js'
|
||||||
import { reject, map, uniqBy, debounce } from 'lodash'
|
import { reject, map, uniqBy, debounce } from 'lodash'
|
||||||
import suggestor from '../emoji_input/suggestor.js'
|
import suggestor from '../emoji_input/suggestor.js'
|
||||||
import { mapGetters, mapState } from 'vuex'
|
import { mapGetters, mapState } from 'vuex'
|
||||||
import Checkbox from '../checkbox/checkbox.vue'
|
import Checkbox from '../checkbox/checkbox.vue'
|
||||||
import Select from '../select/select.vue'
|
import Select from '../select/select.vue'
|
||||||
|
import DraftCloser from 'src/components/draft_closer/draft_closer.vue'
|
||||||
|
|
||||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
import {
|
import {
|
||||||
|
@ -54,6 +56,16 @@ const pxStringToNumber = (str) => {
|
||||||
return Number(str.substring(0, str.length - 2))
|
return Number(str.substring(0, str.length - 2))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const typeAndRefId = ({ replyTo, statusId }) => {
|
||||||
|
if (replyTo) {
|
||||||
|
return ['reply', replyTo]
|
||||||
|
} else if (statusId) {
|
||||||
|
return ['edit', statusId]
|
||||||
|
} else {
|
||||||
|
return ['new', '']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const PostStatusForm = {
|
const PostStatusForm = {
|
||||||
props: [
|
props: [
|
||||||
'statusId',
|
'statusId',
|
||||||
|
@ -86,13 +98,15 @@ const PostStatusForm = {
|
||||||
'fileLimit',
|
'fileLimit',
|
||||||
'submitOnEnter',
|
'submitOnEnter',
|
||||||
'emojiPickerPlacement',
|
'emojiPickerPlacement',
|
||||||
'optimisticPosting'
|
'optimisticPosting',
|
||||||
|
'draftId'
|
||||||
],
|
],
|
||||||
emits: [
|
emits: [
|
||||||
'posted',
|
'posted',
|
||||||
'resize',
|
'resize',
|
||||||
'mediaplay',
|
'mediaplay',
|
||||||
'mediapause'
|
'mediapause',
|
||||||
|
'can-close'
|
||||||
],
|
],
|
||||||
components: {
|
components: {
|
||||||
MediaUpload,
|
MediaUpload,
|
||||||
|
@ -103,7 +117,8 @@ const PostStatusForm = {
|
||||||
Select,
|
Select,
|
||||||
Attachment,
|
Attachment,
|
||||||
StatusContent,
|
StatusContent,
|
||||||
Gallery
|
Gallery,
|
||||||
|
DraftCloser
|
||||||
},
|
},
|
||||||
mounted () {
|
mounted () {
|
||||||
this.updateIdempotencyKey()
|
this.updateIdempotencyKey()
|
||||||
|
@ -124,39 +139,52 @@ const PostStatusForm = {
|
||||||
|
|
||||||
const { scopeCopy } = this.$store.getters.mergedConfig
|
const { scopeCopy } = this.$store.getters.mergedConfig
|
||||||
|
|
||||||
if (this.replyTo) {
|
const [statusType, refId] = typeAndRefId({ replyTo: this.replyTo, statusId: this.statusId })
|
||||||
const currentUser = this.$store.state.users.currentUser
|
|
||||||
statusText = buildMentionsString({ user: this.repliedUser, attentions: this.attentions }, currentUser)
|
|
||||||
}
|
|
||||||
|
|
||||||
const scope = ((this.copyMessageScope && scopeCopy) || this.copyMessageScope === 'direct')
|
// If we are starting a new post, do not associate it with old drafts
|
||||||
? this.copyMessageScope
|
let statusParams = (this.draftId || statusType !== 'new') ? this.getDraft(statusType, refId) : null
|
||||||
: this.$store.state.users.currentUser.default_scope
|
|
||||||
|
|
||||||
const { postContentType: contentType, sensitiveByDefault } = this.$store.getters.mergedConfig
|
if (!statusParams) {
|
||||||
|
if (statusType === 'reply') {
|
||||||
|
const currentUser = this.$store.state.users.currentUser
|
||||||
|
statusText = buildMentionsString({ user: this.repliedUser, attentions: this.attentions }, currentUser)
|
||||||
|
}
|
||||||
|
|
||||||
let statusParams = {
|
const scope = ((this.copyMessageScope && scopeCopy) || this.copyMessageScope === 'direct')
|
||||||
spoilerText: this.subject || '',
|
? this.copyMessageScope
|
||||||
status: statusText,
|
: this.$store.state.users.currentUser.default_scope
|
||||||
nsfw: !!sensitiveByDefault,
|
|
||||||
files: [],
|
const { postContentType: contentType, sensitiveByDefault } = this.$store.getters.mergedConfig
|
||||||
poll: {},
|
|
||||||
mediaDescriptions: {},
|
|
||||||
visibility: scope,
|
|
||||||
contentType
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.statusId) {
|
|
||||||
const statusContentType = this.statusContentType || contentType
|
|
||||||
statusParams = {
|
statusParams = {
|
||||||
|
type: statusType,
|
||||||
|
refId,
|
||||||
spoilerText: this.subject || '',
|
spoilerText: this.subject || '',
|
||||||
status: this.statusText || '',
|
status: statusText,
|
||||||
nsfw: this.statusIsSensitive || !!sensitiveByDefault,
|
nsfw: !!sensitiveByDefault,
|
||||||
files: this.statusFiles || [],
|
files: [],
|
||||||
poll: this.statusPoll || {},
|
poll: {},
|
||||||
mediaDescriptions: this.statusMediaDescriptions || {},
|
hasPoll: false,
|
||||||
visibility: this.statusScope || scope,
|
mediaDescriptions: {},
|
||||||
contentType: statusContentType
|
visibility: scope,
|
||||||
|
contentType
|
||||||
|
}
|
||||||
|
|
||||||
|
if (statusType === 'edit') {
|
||||||
|
const statusContentType = this.statusContentType || contentType
|
||||||
|
statusParams = {
|
||||||
|
type: statusType,
|
||||||
|
refId,
|
||||||
|
spoilerText: this.subject || '',
|
||||||
|
status: this.statusText || '',
|
||||||
|
nsfw: this.statusIsSensitive || !!sensitiveByDefault,
|
||||||
|
files: this.statusFiles || [],
|
||||||
|
poll: this.statusPoll || {},
|
||||||
|
hasPoll: false,
|
||||||
|
mediaDescriptions: this.statusMediaDescriptions || {},
|
||||||
|
visibility: this.statusScope || scope,
|
||||||
|
contentType: statusContentType
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -168,13 +196,14 @@ const PostStatusForm = {
|
||||||
highlighted: 0,
|
highlighted: 0,
|
||||||
newStatus: statusParams,
|
newStatus: statusParams,
|
||||||
caret: 0,
|
caret: 0,
|
||||||
pollFormVisible: false,
|
|
||||||
showDropIcon: 'hide',
|
showDropIcon: 'hide',
|
||||||
dropStopTimeout: null,
|
dropStopTimeout: null,
|
||||||
preview: null,
|
preview: null,
|
||||||
previewLoading: false,
|
previewLoading: false,
|
||||||
emojiInputShown: false,
|
emojiInputShown: false,
|
||||||
idempotencyKey: ''
|
idempotencyKey: '',
|
||||||
|
saveInhibited: true,
|
||||||
|
savable: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
@ -265,6 +294,24 @@ const PostStatusForm = {
|
||||||
isEdit () {
|
isEdit () {
|
||||||
return typeof this.statusId !== 'undefined' && this.statusId.trim() !== ''
|
return typeof this.statusId !== 'undefined' && this.statusId.trim() !== ''
|
||||||
},
|
},
|
||||||
|
debouncedMaybeAutoSaveDraft () {
|
||||||
|
return debounce(this.maybeAutoSaveDraft, 3000)
|
||||||
|
},
|
||||||
|
pollFormVisible () {
|
||||||
|
return this.newStatus.hasPoll
|
||||||
|
},
|
||||||
|
shouldAutoSaveDraft () {
|
||||||
|
return this.$store.getters.mergedConfig.autoSaveDraft
|
||||||
|
},
|
||||||
|
autoSaveState () {
|
||||||
|
if (this.savable) {
|
||||||
|
return this.$t('post_status.auto_save_saving')
|
||||||
|
} else if (this.newStatus.id) {
|
||||||
|
return this.$t('post_status.auto_save_saved')
|
||||||
|
} else {
|
||||||
|
return this.$t('post_status.auto_save_nothing_new')
|
||||||
|
}
|
||||||
|
},
|
||||||
...mapGetters(['mergedConfig']),
|
...mapGetters(['mergedConfig']),
|
||||||
...mapState({
|
...mapState({
|
||||||
mobileLayout: state => state.interface.mobileLayout
|
mobileLayout: state => state.interface.mobileLayout
|
||||||
|
@ -278,13 +325,20 @@ const PostStatusForm = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
beforeUnmount () {
|
||||||
|
this.maybeAutoSaveDraft()
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
statusChanged () {
|
statusChanged () {
|
||||||
this.autoPreview()
|
this.autoPreview()
|
||||||
this.updateIdempotencyKey()
|
this.updateIdempotencyKey()
|
||||||
|
this.debouncedMaybeAutoSaveDraft()
|
||||||
|
this.savable = true
|
||||||
|
this.saveInhibited = false
|
||||||
},
|
},
|
||||||
clearStatus () {
|
clearStatus () {
|
||||||
const newStatus = this.newStatus
|
const newStatus = this.newStatus
|
||||||
|
this.saveInhibited = true
|
||||||
this.newStatus = {
|
this.newStatus = {
|
||||||
status: '',
|
status: '',
|
||||||
spoilerText: '',
|
spoilerText: '',
|
||||||
|
@ -292,9 +346,9 @@ const PostStatusForm = {
|
||||||
visibility: newStatus.visibility,
|
visibility: newStatus.visibility,
|
||||||
contentType: newStatus.contentType,
|
contentType: newStatus.contentType,
|
||||||
poll: {},
|
poll: {},
|
||||||
|
hasPoll: false,
|
||||||
mediaDescriptions: {}
|
mediaDescriptions: {}
|
||||||
}
|
}
|
||||||
this.pollFormVisible = false
|
|
||||||
this.$refs.mediaUpload && this.$refs.mediaUpload.clearFile()
|
this.$refs.mediaUpload && this.$refs.mediaUpload.clearFile()
|
||||||
this.clearPollForm()
|
this.clearPollForm()
|
||||||
if (this.preserveFocus) {
|
if (this.preserveFocus) {
|
||||||
|
@ -307,6 +361,7 @@ const PostStatusForm = {
|
||||||
el.style.height = undefined
|
el.style.height = undefined
|
||||||
this.error = null
|
this.error = null
|
||||||
if (this.preview) this.previewStatus()
|
if (this.preview) this.previewStatus()
|
||||||
|
this.savable = false
|
||||||
},
|
},
|
||||||
async postStatus (event, newStatus, opts = {}) {
|
async postStatus (event, newStatus, opts = {}) {
|
||||||
if (this.posting && !this.optimisticPosting) { return }
|
if (this.posting && !this.optimisticPosting) { return }
|
||||||
|
@ -324,7 +379,7 @@ const PostStatusForm = {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const poll = this.pollFormVisible ? this.newStatus.poll : {}
|
const poll = this.newStatus.hasPoll ? pollFormToMasto(this.newStatus.poll) : {}
|
||||||
if (this.pollContentError) {
|
if (this.pollContentError) {
|
||||||
this.error = this.pollContentError
|
this.error = this.pollContentError
|
||||||
return
|
return
|
||||||
|
@ -357,6 +412,7 @@ const PostStatusForm = {
|
||||||
|
|
||||||
postHandler(postingOptions).then((data) => {
|
postHandler(postingOptions).then((data) => {
|
||||||
if (!data.error) {
|
if (!data.error) {
|
||||||
|
this.abandonDraft()
|
||||||
this.clearStatus()
|
this.clearStatus()
|
||||||
this.$emit('posted', data)
|
this.$emit('posted', data)
|
||||||
} else {
|
} else {
|
||||||
|
@ -600,7 +656,7 @@ const PostStatusForm = {
|
||||||
this.newStatus.visibility = visibility
|
this.newStatus.visibility = visibility
|
||||||
},
|
},
|
||||||
togglePollForm () {
|
togglePollForm () {
|
||||||
this.pollFormVisible = !this.pollFormVisible
|
this.newStatus.hasPoll = !this.newStatus.hasPoll
|
||||||
},
|
},
|
||||||
setPoll (poll) {
|
setPoll (poll) {
|
||||||
this.newStatus.poll = poll
|
this.newStatus.poll = poll
|
||||||
|
@ -633,6 +689,60 @@ const PostStatusForm = {
|
||||||
},
|
},
|
||||||
propsToNative (props) {
|
propsToNative (props) {
|
||||||
return propsToNative(props)
|
return propsToNative(props)
|
||||||
|
},
|
||||||
|
saveDraft () {
|
||||||
|
if (!this.saveInhibited &&
|
||||||
|
(this.newStatus.status ||
|
||||||
|
this.newStatus.files?.length ||
|
||||||
|
this.newStatus.hasPoll
|
||||||
|
)) {
|
||||||
|
return this.$store.dispatch('addOrSaveDraft', { draft: this.newStatus })
|
||||||
|
.then(id => {
|
||||||
|
if (this.newStatus.id !== id) {
|
||||||
|
this.newStatus.id = id
|
||||||
|
}
|
||||||
|
this.savable = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return Promise.resolve()
|
||||||
|
},
|
||||||
|
maybeAutoSaveDraft () {
|
||||||
|
if (this.shouldAutoSaveDraft) {
|
||||||
|
this.saveDraft()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
abandonDraft () {
|
||||||
|
return this.$store.dispatch('abandonDraft', { id: this.newStatus.id })
|
||||||
|
},
|
||||||
|
getDraft (statusType, refId) {
|
||||||
|
const maybeDraft = this.$store.state.drafts.drafts[this.draftId]
|
||||||
|
if (this.draftId && maybeDraft) {
|
||||||
|
return maybeDraft
|
||||||
|
} else {
|
||||||
|
const existingDrafts = this.$store.getters.draftsByTypeAndRefId(statusType, refId)
|
||||||
|
|
||||||
|
if (existingDrafts.length) {
|
||||||
|
return existingDrafts[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// No draft available, fall back
|
||||||
|
},
|
||||||
|
requestClose () {
|
||||||
|
if (!this.savable) {
|
||||||
|
this.$emit('can-close')
|
||||||
|
} else {
|
||||||
|
this.$refs.draftCloser.requestClose()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
saveAndCloseDraft () {
|
||||||
|
this.saveDraft().then(() => {
|
||||||
|
this.$emit('can-close')
|
||||||
|
})
|
||||||
|
},
|
||||||
|
discardAndCloseDraft () {
|
||||||
|
this.abandonDraft().then(() => {
|
||||||
|
this.$emit('can-close')
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -233,7 +233,7 @@
|
||||||
v-if="pollsAvailable"
|
v-if="pollsAvailable"
|
||||||
ref="pollForm"
|
ref="pollForm"
|
||||||
:visible="pollFormVisible"
|
:visible="pollFormVisible"
|
||||||
@update-poll="setPoll"
|
:params="newStatus.poll"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
ref="bottom"
|
ref="bottom"
|
||||||
|
@ -267,6 +267,19 @@
|
||||||
<FAIcon icon="poll-h" />
|
<FAIcon icon="poll-h" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<span
|
||||||
|
v-if="shouldAutoSaveDraft"
|
||||||
|
class="auto-save-status"
|
||||||
|
>
|
||||||
|
{{ autoSaveState }}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
v-else
|
||||||
|
class="btn button-default"
|
||||||
|
@click="saveDraft"
|
||||||
|
>
|
||||||
|
{{ $t('post_status.save_to_drafts_button') }}
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="posting"
|
v-if="posting"
|
||||||
disabled
|
disabled
|
||||||
|
@ -339,6 +352,11 @@
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
<DraftCloser
|
||||||
|
ref="draftCloser"
|
||||||
|
@save="saveAndCloseDraft"
|
||||||
|
@discard="discardAndCloseDraft"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -608,5 +626,9 @@
|
||||||
border: 2px dashed $fallback--text;
|
border: 2px dashed $fallback--text;
|
||||||
border: 2px dashed var(--text, $fallback--text);
|
border: 2px dashed var(--text, $fallback--text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.auto-save-status {
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -50,6 +50,11 @@ const GeneralTab = {
|
||||||
value: mode,
|
value: mode,
|
||||||
label: this.$t(`settings.user_popover_avatar_action_${mode}`)
|
label: this.$t(`settings.user_popover_avatar_action_${mode}`)
|
||||||
})),
|
})),
|
||||||
|
unsavedPostActionOptions: ['save', 'discard', 'confirm'].map(mode => ({
|
||||||
|
key: mode,
|
||||||
|
value: mode,
|
||||||
|
label: this.$t(`settings.unsaved_post_action_${mode}`)
|
||||||
|
})),
|
||||||
loopSilentAvailable:
|
loopSilentAvailable:
|
||||||
// Firefox
|
// Firefox
|
||||||
Object.getOwnPropertyDescriptor(HTMLVideoElement.prototype, 'mozHasAudio') ||
|
Object.getOwnPropertyDescriptor(HTMLVideoElement.prototype, 'mozHasAudio') ||
|
||||||
|
|
|
@ -518,6 +518,22 @@
|
||||||
{{ $t('settings.autocomplete_select_first') }}
|
{{ $t('settings.autocomplete_select_first') }}
|
||||||
</BooleanSetting>
|
</BooleanSetting>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<BooleanSetting
|
||||||
|
path="autoSaveDraft"
|
||||||
|
>
|
||||||
|
{{ $t('settings.auto_save_draft') }}
|
||||||
|
</BooleanSetting>
|
||||||
|
</li>
|
||||||
|
<li v-if="!autoSaveDraft">
|
||||||
|
<ChoiceSetting
|
||||||
|
id="unsavedPostAction"
|
||||||
|
path="unsavedPostAction"
|
||||||
|
:options="unsavedPostActionOptions"
|
||||||
|
>
|
||||||
|
{{ $t('settings.unsaved_post_action') }}
|
||||||
|
</ChoiceSetting>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -17,7 +17,8 @@ import {
|
||||||
faCog,
|
faCog,
|
||||||
faInfoCircle,
|
faInfoCircle,
|
||||||
faCompass,
|
faCompass,
|
||||||
faList
|
faList,
|
||||||
|
faFilePen
|
||||||
} from '@fortawesome/free-solid-svg-icons'
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
library.add(
|
library.add(
|
||||||
|
@ -33,7 +34,8 @@ library.add(
|
||||||
faCog,
|
faCog,
|
||||||
faInfoCircle,
|
faInfoCircle,
|
||||||
faCompass,
|
faCompass,
|
||||||
faList
|
faList,
|
||||||
|
faFilePen
|
||||||
)
|
)
|
||||||
|
|
||||||
const SideDrawer = {
|
const SideDrawer = {
|
||||||
|
@ -98,7 +100,7 @@ const SideDrawer = {
|
||||||
pleromaChatMessagesAvailable: state => state.instance.pleromaChatMessagesAvailable,
|
pleromaChatMessagesAvailable: state => state.instance.pleromaChatMessagesAvailable,
|
||||||
supportsAnnouncements: state => state.announcements.supportsAnnouncements
|
supportsAnnouncements: state => state.announcements.supportsAnnouncements
|
||||||
}),
|
}),
|
||||||
...mapGetters(['unreadChatCount', 'unreadAnnouncementCount'])
|
...mapGetters(['unreadChatCount', 'unreadAnnouncementCount', 'draftCount'])
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
toggleDrawer () {
|
toggleDrawer () {
|
||||||
|
|
|
@ -211,6 +211,26 @@
|
||||||
</span>
|
</span>
|
||||||
</router-link>
|
</router-link>
|
||||||
</li>
|
</li>
|
||||||
|
<li
|
||||||
|
v-if="currentUser"
|
||||||
|
@click="toggleDrawer"
|
||||||
|
>
|
||||||
|
<router-link
|
||||||
|
:to="{ name: 'drafts' }"
|
||||||
|
>
|
||||||
|
<FAIcon
|
||||||
|
fixed-width
|
||||||
|
class="fa-scale-110 fa-old-padding"
|
||||||
|
icon="file-pen"
|
||||||
|
/> {{ $t('nav.drafts') }}
|
||||||
|
<span
|
||||||
|
v-if="draftCount"
|
||||||
|
class="badge badge-neutral"
|
||||||
|
>
|
||||||
|
{{ draftCount }}
|
||||||
|
</span>
|
||||||
|
</router-link>
|
||||||
|
</li>
|
||||||
<li
|
<li
|
||||||
v-if="currentUser"
|
v-if="currentUser"
|
||||||
@click="toggleDrawer"
|
@click="toggleDrawer"
|
||||||
|
|
|
@ -423,6 +423,13 @@ const Status = {
|
||||||
this.error = undefined
|
this.error = undefined
|
||||||
},
|
},
|
||||||
toggleReplying () {
|
toggleReplying () {
|
||||||
|
if (this.replying) {
|
||||||
|
this.$refs.postStatusForm.requestClose()
|
||||||
|
} else {
|
||||||
|
this.doToggleReplying()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
doToggleReplying () {
|
||||||
controlledOrUncontrolledToggle(this, 'replying')
|
controlledOrUncontrolledToggle(this, 'replying')
|
||||||
},
|
},
|
||||||
gotoOriginal (id) {
|
gotoOriginal (id) {
|
||||||
|
|
|
@ -496,13 +496,15 @@
|
||||||
class="status-container reply-form"
|
class="status-container reply-form"
|
||||||
>
|
>
|
||||||
<PostStatusForm
|
<PostStatusForm
|
||||||
|
ref="postStatusForm"
|
||||||
class="reply-body"
|
class="reply-body"
|
||||||
:reply-to="status.id"
|
:reply-to="status.id"
|
||||||
:attentions="status.attentions"
|
:attentions="status.attentions"
|
||||||
:replied-user="status.user"
|
:replied-user="status.user"
|
||||||
:copy-message-scope="status.visibility"
|
:copy-message-scope="status.visibility"
|
||||||
:subject="replySubject"
|
:subject="replySubject"
|
||||||
@posted="toggleReplying"
|
@posted="doToggleReplying"
|
||||||
|
@can-close="doToggleReplying"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -189,7 +189,8 @@
|
||||||
"mobile_notifications": "Open notifications",
|
"mobile_notifications": "Open notifications",
|
||||||
"mobile_notifications": "Open notifications (there are unread ones)",
|
"mobile_notifications": "Open notifications (there are unread ones)",
|
||||||
"mobile_notifications_close": "Close notifications",
|
"mobile_notifications_close": "Close notifications",
|
||||||
"announcements": "Announcements"
|
"announcements": "Announcements",
|
||||||
|
"drafts": "Drafts"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"broken_favorite": "Unknown status, searching for it…",
|
"broken_favorite": "Unknown status, searching for it…",
|
||||||
|
@ -295,7 +296,16 @@
|
||||||
"private": "Followers-only - post to followers only",
|
"private": "Followers-only - post to followers only",
|
||||||
"public": "Public - post to public timelines",
|
"public": "Public - post to public timelines",
|
||||||
"unlisted": "Unlisted - do not post to public timelines"
|
"unlisted": "Unlisted - do not post to public timelines"
|
||||||
}
|
},
|
||||||
|
"close_confirm_title": "Closing post form",
|
||||||
|
"close_confirm": "What do you want to do with your current writing?",
|
||||||
|
"close_confirm_save_button": "Save",
|
||||||
|
"close_confirm_discard_button": "Discard",
|
||||||
|
"close_confirm_continue_composing_button": "Continue composing",
|
||||||
|
"auto_save_nothing_new": "Nothing new to save.",
|
||||||
|
"auto_save_saved": "Saved.",
|
||||||
|
"auto_save_saving": "Saving...",
|
||||||
|
"save_to_drafts_button": "Save to drafts"
|
||||||
},
|
},
|
||||||
"registration": {
|
"registration": {
|
||||||
"bio_optional": "Bio (optional)",
|
"bio_optional": "Bio (optional)",
|
||||||
|
@ -466,6 +476,11 @@
|
||||||
"avatar_size_instruction": "The recommended minimum size for avatar images is 150x150 pixels.",
|
"avatar_size_instruction": "The recommended minimum size for avatar images is 150x150 pixels.",
|
||||||
"pad_emoji": "Pad emoji with spaces when adding from picker",
|
"pad_emoji": "Pad emoji with spaces when adding from picker",
|
||||||
"autocomplete_select_first": "Automatically select the first candidate when autocomplete results are available",
|
"autocomplete_select_first": "Automatically select the first candidate when autocomplete results are available",
|
||||||
|
"unsaved_post_action": "When you try to close an unsaved posting form",
|
||||||
|
"unsaved_post_action_save": "Save it to drafts",
|
||||||
|
"unsaved_post_action_discard": "Discard it",
|
||||||
|
"unsaved_post_action_confirm": "Ask every time",
|
||||||
|
"auto_save_draft": "Save drafts as you compose",
|
||||||
"emoji_reactions_on_timeline": "Show emoji reactions on timeline",
|
"emoji_reactions_on_timeline": "Show emoji reactions on timeline",
|
||||||
"emoji_reactions_scale": "Reactions scale factor",
|
"emoji_reactions_scale": "Reactions scale factor",
|
||||||
"export_theme": "Save preset",
|
"export_theme": "Save preset",
|
||||||
|
@ -1154,5 +1169,17 @@
|
||||||
},
|
},
|
||||||
"unicode_domain_indicator": {
|
"unicode_domain_indicator": {
|
||||||
"tooltip": "This domain contains non-ascii characters."
|
"tooltip": "This domain contains non-ascii characters."
|
||||||
|
},
|
||||||
|
"drafts": {
|
||||||
|
"drafts": "Drafts",
|
||||||
|
"continue": "Continue composing",
|
||||||
|
"abandon": "Abandon draft",
|
||||||
|
"abandon_confirm_title": "Abandon confirmation",
|
||||||
|
"abandon_confirm": "Do you really want to abandon this draft?",
|
||||||
|
"abandon_confirm_accept_button": "Abandon",
|
||||||
|
"abandon_confirm_cancel_button": "Keep",
|
||||||
|
"replying": "Replying to {statusLink}",
|
||||||
|
"editing": "Editing {statusLink}",
|
||||||
|
"unavailable": "(unavailable)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import merge from 'lodash.merge'
|
import merge from 'lodash.merge'
|
||||||
import localforage from 'localforage'
|
|
||||||
import { each, get, set, cloneDeep } from 'lodash'
|
import { each, get, set, cloneDeep } from 'lodash'
|
||||||
|
import { storage } from './storage.js'
|
||||||
|
|
||||||
let loaded = false
|
let loaded = false
|
||||||
|
|
||||||
|
@ -26,7 +26,7 @@ const saveImmedeatelyActions = [
|
||||||
]
|
]
|
||||||
|
|
||||||
const defaultStorage = (() => {
|
const defaultStorage = (() => {
|
||||||
return localforage
|
return storage
|
||||||
})()
|
})()
|
||||||
|
|
||||||
export default function createPersistedState ({
|
export default function createPersistedState ({
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
import localforage from 'localforage'
|
||||||
|
|
||||||
|
export const storage = localforage
|
|
@ -22,6 +22,7 @@ import pollsModule from './modules/polls.js'
|
||||||
import postStatusModule from './modules/postStatus.js'
|
import postStatusModule from './modules/postStatus.js'
|
||||||
import editStatusModule from './modules/editStatus.js'
|
import editStatusModule from './modules/editStatus.js'
|
||||||
import statusHistoryModule from './modules/statusHistory.js'
|
import statusHistoryModule from './modules/statusHistory.js'
|
||||||
|
import draftsModule from './modules/drafts.js'
|
||||||
|
|
||||||
import chatsModule from './modules/chats.js'
|
import chatsModule from './modules/chats.js'
|
||||||
import announcementsModule from './modules/announcements.js'
|
import announcementsModule from './modules/announcements.js'
|
||||||
|
@ -92,6 +93,7 @@ const persistedStateOptions = {
|
||||||
postStatus: postStatusModule,
|
postStatus: postStatusModule,
|
||||||
editStatus: editStatusModule,
|
editStatus: editStatusModule,
|
||||||
statusHistory: statusHistoryModule,
|
statusHistory: statusHistoryModule,
|
||||||
|
drafts: draftsModule,
|
||||||
chats: chatsModule,
|
chats: chatsModule,
|
||||||
announcements: announcementsModule
|
announcements: announcementsModule
|
||||||
},
|
},
|
||||||
|
|
|
@ -18,7 +18,8 @@ export const multiChoiceProperties = [
|
||||||
'conversationDisplay', // tree | linear
|
'conversationDisplay', // tree | linear
|
||||||
'conversationOtherRepliesButton', // below | inside
|
'conversationOtherRepliesButton', // below | inside
|
||||||
'mentionLinkDisplay', // short | full_for_remote | full
|
'mentionLinkDisplay', // short | full_for_remote | full
|
||||||
'userPopoverAvatarAction' // close | zoom | open
|
'userPopoverAvatarAction', // close | zoom | open
|
||||||
|
'unsavedPostAction' // save | discard | confirm
|
||||||
]
|
]
|
||||||
|
|
||||||
export const defaultState = {
|
export const defaultState = {
|
||||||
|
@ -117,7 +118,9 @@ export const defaultState = {
|
||||||
conversationOtherRepliesButton: undefined, // instance default
|
conversationOtherRepliesButton: undefined, // instance default
|
||||||
conversationTreeFadeAncestors: undefined, // instance default
|
conversationTreeFadeAncestors: undefined, // instance default
|
||||||
maxDepthInThread: undefined, // instance default
|
maxDepthInThread: undefined, // instance default
|
||||||
autocompleteSelect: undefined // instance default
|
autocompleteSelect: undefined, // instance default
|
||||||
|
unsavedPostAction: undefined, // instance default
|
||||||
|
autoSaveDraft: undefined // instance default
|
||||||
}
|
}
|
||||||
|
|
||||||
// caching the instance default properties
|
// caching the instance default properties
|
||||||
|
|
|
@ -0,0 +1,86 @@
|
||||||
|
import { storage } from 'src/lib/storage.js'
|
||||||
|
|
||||||
|
export const defaultState = {
|
||||||
|
drafts: {}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const mutations = {
|
||||||
|
addOrSaveDraft (state, { draft }) {
|
||||||
|
state.drafts[draft.id] = draft
|
||||||
|
},
|
||||||
|
abandonDraft (state, { id }) {
|
||||||
|
delete state.drafts[id]
|
||||||
|
},
|
||||||
|
loadDrafts (state, data) {
|
||||||
|
state.drafts = data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const storageKey = 'pleroma-fe-drafts'
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Note: we do not use the persist state plugin because
|
||||||
|
* it is not impossible for a user to have two windows at
|
||||||
|
* the same time. The persist state plugin is just overriding
|
||||||
|
* everything with the current state. This isn't good because
|
||||||
|
* if a draft is created in one window and another draft is
|
||||||
|
* created in another, the draft in the first window will just
|
||||||
|
* be overriden.
|
||||||
|
* Here, we can't guarantee 100% atomicity unless one uses
|
||||||
|
* different keys, which will just pollute the whole storage.
|
||||||
|
* It is indeed best to have backend support for this.
|
||||||
|
*/
|
||||||
|
const getStorageData = async () => ((await storage.getItem(storageKey)) || {})
|
||||||
|
|
||||||
|
const saveDraftToStorage = async (draft) => {
|
||||||
|
const currentData = await getStorageData()
|
||||||
|
currentData[draft.id] = JSON.parse(JSON.stringify(draft))
|
||||||
|
await storage.setItem(storageKey, currentData)
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteDraftFromStorage = async (id) => {
|
||||||
|
const currentData = await getStorageData()
|
||||||
|
delete currentData[id]
|
||||||
|
await storage.setItem(storageKey, currentData)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const actions = {
|
||||||
|
async addOrSaveDraft (store, { draft }) {
|
||||||
|
const id = draft.id || (new Date().getTime()).toString()
|
||||||
|
const draftWithId = { ...draft, id }
|
||||||
|
store.commit('addOrSaveDraft', { draft: draftWithId })
|
||||||
|
await saveDraftToStorage(draftWithId)
|
||||||
|
return id
|
||||||
|
},
|
||||||
|
async abandonDraft (store, { id }) {
|
||||||
|
store.commit('abandonDraft', { id })
|
||||||
|
await deleteDraftFromStorage(id)
|
||||||
|
},
|
||||||
|
async loadDrafts (store) {
|
||||||
|
const currentData = await getStorageData()
|
||||||
|
store.commit('loadDrafts', currentData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getters = {
|
||||||
|
draftsByTypeAndRefId (state) {
|
||||||
|
return (type, refId) => {
|
||||||
|
return Object.values(state.drafts).filter(draft => draft.type === type && draft.refId === refId)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
draftsArray (state) {
|
||||||
|
return Object.values(state.drafts)
|
||||||
|
},
|
||||||
|
draftCount (state) {
|
||||||
|
return Object.values(state.drafts).length
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const drafts = {
|
||||||
|
state: defaultState,
|
||||||
|
mutations,
|
||||||
|
getters,
|
||||||
|
actions
|
||||||
|
}
|
||||||
|
|
||||||
|
export default drafts
|
|
@ -105,6 +105,8 @@ const defaultState = {
|
||||||
conversationTreeFadeAncestors: false,
|
conversationTreeFadeAncestors: false,
|
||||||
maxDepthInThread: 6,
|
maxDepthInThread: 6,
|
||||||
autocompleteSelect: false,
|
autocompleteSelect: false,
|
||||||
|
unsavedPostAction: 'confirm',
|
||||||
|
autoSaveDraft: false,
|
||||||
|
|
||||||
// Nasty stuff
|
// Nasty stuff
|
||||||
customEmoji: [],
|
customEmoji: [],
|
||||||
|
|
|
@ -0,0 +1,36 @@
|
||||||
|
import * as DateUtils from 'src/services/date_utils/date_utils.js'
|
||||||
|
import { uniq } from 'lodash'
|
||||||
|
|
||||||
|
const pollFallbackValues = {
|
||||||
|
pollType: 'single',
|
||||||
|
options: ['', ''],
|
||||||
|
expiryAmount: 10,
|
||||||
|
expiryUnit: 'minutes'
|
||||||
|
}
|
||||||
|
|
||||||
|
const pollFallback = (object, attr) => {
|
||||||
|
return object[attr] !== undefined ? object[attr] : pollFallbackValues[attr]
|
||||||
|
}
|
||||||
|
|
||||||
|
const pollFormToMasto = (poll) => {
|
||||||
|
const expiresIn = DateUtils.unitToSeconds(
|
||||||
|
pollFallback(poll, 'expiryUnit'),
|
||||||
|
pollFallback(poll, 'expiryAmount')
|
||||||
|
)
|
||||||
|
|
||||||
|
const options = uniq(pollFallback(poll, 'options').filter(option => option !== ''))
|
||||||
|
if (options.length < 2) {
|
||||||
|
return { errorKey: 'polls.not_enough_options' }
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
options,
|
||||||
|
multiple: pollFallback(poll, 'pollType') === 'multiple',
|
||||||
|
expiresIn
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
pollFallback,
|
||||||
|
pollFormToMasto
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
/* eslint-env serviceworker */
|
/* eslint-env serviceworker */
|
||||||
|
|
||||||
import localForage from 'localforage'
|
import { storage } from 'src/lib/storage.js'
|
||||||
import { parseNotification } from './services/entity_normalizer/entity_normalizer.service.js'
|
import { parseNotification } from './services/entity_normalizer/entity_normalizer.service.js'
|
||||||
import { prepareNotificationObject } from './services/notification_utils/notification_utils.js'
|
import { prepareNotificationObject } from './services/notification_utils/notification_utils.js'
|
||||||
import { createI18n } from 'vue-i18n'
|
import { createI18n } from 'vue-i18n'
|
||||||
|
@ -14,7 +14,7 @@ const i18n = createI18n({
|
||||||
})
|
})
|
||||||
|
|
||||||
function isEnabled () {
|
function isEnabled () {
|
||||||
return localForage.getItem('vuex-lz')
|
return storage.getItem('vuex-lz')
|
||||||
.then(data => data.config.webPushNotifications)
|
.then(data => data.config.webPushNotifications)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,7 +24,7 @@ function getWindowClients () {
|
||||||
}
|
}
|
||||||
|
|
||||||
const setLocale = async () => {
|
const setLocale = async () => {
|
||||||
const state = await localForage.getItem('vuex-lz')
|
const state = await storage.getItem('vuex-lz')
|
||||||
const locale = state.config.interfaceLanguage || 'en'
|
const locale = state.config.interfaceLanguage || 'en'
|
||||||
i18n.locale = locale
|
i18n.locale = locale
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue