diff --git a/changelog.d/drafts-imp.skip b/changelog.d/drafts-imp.skip new file mode 100644 index 000000000..e69de29bb diff --git a/src/App.scss b/src/App.scss index 2afc43908..1b781304e 100644 --- a/src/App.scss +++ b/src/App.scss @@ -390,6 +390,24 @@ nav { } } +.menu-item { + line-height: var(--__line-height); + font-family: inherit; + font-weight: 400; + font-size: 100%; + cursor: pointer; + + a, + button:not(.button-default) { + color: var(--text); + font-size: 100%; + } + + &.disabled { + cursor: not-allowed; + } +} + .menu-item, .list-item { display: block; @@ -397,10 +415,6 @@ nav { border: none; outline: none; text-align: initial; - font-size: inherit; - font-family: inherit; - font-weight: 400; - cursor: pointer; color: inherit; clear: both; position: relative; @@ -410,7 +424,6 @@ nav { border-width: 0; border-top-width: 1px; width: 100%; - line-height: var(--__line-height); padding: var(--__vertical-gap) var(--__horizontal-gap); background: transparent; @@ -450,10 +463,8 @@ nav { border: none; outline: none; display: inline; - font-size: 100%; font-family: inherit; line-height: unset; - color: var(--text); } &:first-child { diff --git a/src/components/draft/draft.js b/src/components/draft/draft.js index cb07ec5c5..55ee11a15 100644 --- a/src/components/draft/draft.js +++ b/src/components/draft/draft.js @@ -2,13 +2,25 @@ 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' +import Gallery from 'src/components/gallery/gallery.vue' +import { cloneDeep } from 'lodash' + +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faPollH +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faPollH +) const Draft = { components: { PostStatusForm, EditStatusForm, ConfirmModal, - StatusContent + StatusContent, + Gallery }, props: { draft: { @@ -18,6 +30,7 @@ const Draft = { }, data () { return { + referenceDraft: cloneDeep(this.draft), editing: false, showingConfirmDialog: false } @@ -32,6 +45,11 @@ const Draft = { return {} } }, + safeToSave () { + return this.draft.status || + this.draft.files?.length || + this.draft.hasPoll + }, postStatusFormProps () { return { draftId: this.draft.id, @@ -40,6 +58,28 @@ const Draft = { }, refStatus () { return this.draft.refId ? this.$store.state.statuses.allStatusesObject[this.draft.refId] : undefined + }, + localCollapseSubjectDefault () { + return this.$store.getters.mergedConfig.collapseMessageWithSubject + }, + nsfwClickthrough () { + if (!this.draft.nsfw) { + return false + } + if (this.draft.summary && this.localCollapseSubjectDefault) { + return false + } + return true + } + }, + watch: { + editing (newVal) { + if (newVal) return + if (this.safeToSave) { + this.$store.dispatch('addOrSaveDraft', { draft: this.draft }) + } else { + this.$store.dispatch('addOrSaveDraft', { draft: this.referenceDraft }) + } } }, methods: { diff --git a/src/components/draft/draft.vue b/src/components/draft/draft.vue index d9d356121..46227b684 100644 --- a/src/components/draft/draft.vue +++ b/src/components/draft/draft.vue @@ -1,21 +1,5 @@ @@ -73,17 +107,55 @@ diff --git a/src/components/menu_item.style.js b/src/components/menu_item.style.js index 5b3ff699c..d7fe0f7c7 100644 --- a/src/components/menu_item.style.js +++ b/src/components/menu_item.style.js @@ -11,8 +11,9 @@ export default { 'Avatar' ], states: { - hover: ':hover', - active: '.-active' + hover: ':hover:not(.disabled)', + active: '.-active', + disabled: '.disabled' }, defaultRules: [ { @@ -85,6 +86,28 @@ export default { textColor: '--link', textAuto: 'no-preserve' } + }, + { + component: 'Text', + parent: { + component: 'MenuItem', + state: ['disabled'] + }, + directives: { + textOpacity: 0.25, + textOpacityMode: 'blend' + } + }, + { + component: 'Icon', + parent: { + component: 'MenuItem', + state: ['disabled'] + }, + directives: { + textOpacity: 0.25, + textOpacityMode: 'blend' + } } ] } diff --git a/src/components/mobile_nav/mobile_nav.vue b/src/components/mobile_nav/mobile_nav.vue index b737155e5..d5539ad89 100644 --- a/src/components/mobile_nav/mobile_nav.vue +++ b/src/components/mobile_nav/mobile_nav.vue @@ -202,6 +202,9 @@ .title { font-size: 1.3em; margin-left: 0.6em; + white-space: nowrap; + overflow-x: hidden; + text-overflow: ellipsis; } } diff --git a/src/components/nav_panel/nav_panel.vue b/src/components/nav_panel/nav_panel.vue index 876e97726..f21b43e8b 100644 --- a/src/components/nav_panel/nav_panel.vue +++ b/src/components/nav_panel/nav_panel.vue @@ -154,11 +154,6 @@ font-size: 1.1em; } - .timelines-chevron { - margin-left: 0.8em; - font-size: 1.1em; - } - .timelines-background { padding: 0 0 0 0.6em; } diff --git a/src/components/navigation/navigation_entry.vue b/src/components/navigation/navigation_entry.vue index 4b6c8e299..bd72bd0d1 100644 --- a/src/components/navigation/navigation_entry.vue +++ b/src/components/navigation/navigation_entry.vue @@ -82,8 +82,12 @@ --__horizontal-gap: 0.5em; --__vertical-gap: 0.4em; - padding: 0; - display: flex; + padding: var(--__vertical-gap) var(--__horizontal-gap); + display: grid; + grid-template-columns: 1fr; + grid-auto-columns: var(--__line-height); + grid-auto-flow: column; + grid-gap: var(--__horizontal-gap); align-items: baseline; &[aria-expanded] { @@ -93,8 +97,6 @@ .main-link { line-height: var(--__line-height); box-sizing: border-box; - flex: 1; - padding: var(--__vertical-gap) var(--__horizontal-gap); } .menu-icon { @@ -104,26 +106,16 @@ margin-right: var(--__horizontal-gap); } - .timelines-chevron { - line-height: var(--__line-height); - padding: 0; - width: var(--__line-height); - margin-right: 0; - } - + .timelines-chevron, .extra-button { line-height: var(--__line-height); + width: 100%; padding: 0; - width: var(--__line-height); text-align: center; - - &:last-child { - margin-right: calc(-1 * var(--__horizontal-gap)); - } } .badge { - margin: 0 var(--__horizontal-gap); + justify-self: center; } .iconEmoji { diff --git a/src/components/post_status_form/post_status_form.js b/src/components/post_status_form/post_status_form.js index 1ae985498..c31b86be0 100644 --- a/src/components/post_status_form/post_status_form.js +++ b/src/components/post_status_form/post_status_form.js @@ -7,6 +7,7 @@ import PollForm from '../poll/poll_form.vue' import Attachment from '../attachment/attachment.vue' import Gallery from 'src/components/gallery/gallery.vue' import StatusContent from '../status_content/status_content.vue' +import Popover from 'src/components/popover/popover.vue' import fileTypeService from '../../services/file_type/file_type.service.js' import { findOffset } from '../../services/offset_finder/offset_finder.service.js' import { propsToNative } from '../../services/attributes_helper/attributes_helper.service.js' @@ -25,7 +26,10 @@ import { faUpload, faBan, faTimes, - faCircleNotch + faCircleNotch, + faChevronDown, + faChevronLeft, + faChevronRight } from '@fortawesome/free-solid-svg-icons' library.add( @@ -34,7 +38,10 @@ library.add( faUpload, faBan, faTimes, - faCircleNotch + faCircleNotch, + faChevronDown, + faChevronLeft, + faChevronRight ) const buildMentionsString = ({ user, attentions = [] }, currentUser) => { @@ -111,7 +118,8 @@ const PostStatusForm = { 'resize', 'mediaplay', 'mediapause', - 'can-close' + 'can-close', + 'update' ], components: { MediaUpload, @@ -123,7 +131,8 @@ const PostStatusForm = { Attachment, StatusContent, Gallery, - DraftCloser + DraftCloser, + Popover }, mounted () { this.updateIdempotencyKey() @@ -210,7 +219,7 @@ const PostStatusForm = { emojiInputShown: false, idempotencyKey: '', saveInhibited: true, - savable: false + saveable: false } }, computed: { @@ -335,7 +344,7 @@ const PostStatusForm = { return this.$store.getters.mergedConfig.autoSaveDraft }, autoSaveState () { - if (this.savable) { + if (this.saveable) { return this.$t('post_status.auto_save_saving') } else if (this.newStatus.id) { return this.$t('post_status.auto_save_saved') @@ -343,6 +352,12 @@ const PostStatusForm = { return this.$t('post_status.auto_save_nothing_new') } }, + safeToSaveDraft () { + return this.newStatus.status || + this.newStatus.spoilerText || + this.newStatus.files?.length || + this.newStatus.hasPoll + }, ...mapGetters(['mergedConfig']), ...mapState({ mobileLayout: state => state.interface.mobileLayout @@ -355,7 +370,7 @@ const PostStatusForm = { this.statusChanged() } }, - savable (val) { + saveable (val) { // https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event#usage_notes // MDN says we'd better add the beforeunload event listener only when needed, and remove it when it's no longer needed if (val) { @@ -374,7 +389,7 @@ const PostStatusForm = { this.autoPreview() this.updateIdempotencyKey() this.debouncedMaybeAutoSaveDraft() - this.savable = true + this.saveable = true this.saveInhibited = false }, clearStatus () { @@ -403,7 +418,7 @@ const PostStatusForm = { el.style.height = undefined this.error = null if (this.preview) this.previewStatus() - this.savable = false + this.saveable = false }, async postStatus (event, newStatus, opts = {}) { if (this.posting && !this.optimisticPosting) { return } @@ -738,21 +753,19 @@ const PostStatusForm = { saveDraft () { if (!this.disableDraft && !this.saveInhibited) { - if (this.newStatus.status || - this.newStatus.files?.length || - this.newStatus.hasPoll) { + if (this.safeToSaveDraft) { return this.$store.dispatch('addOrSaveDraft', { draft: this.newStatus }) .then(id => { if (this.newStatus.id !== id) { this.newStatus.id = id } - this.savable = false + this.saveable = false }) } else if (this.newStatus.id) { // There is a draft, but there is nothing in it, clear it return this.abandonDraft() .then(() => { - this.savable = false + this.saveable = false }) } } @@ -780,7 +793,7 @@ const PostStatusForm = { // No draft available, fall back }, requestClose () { - if (!this.savable) { + if (!this.saveable) { this.$emit('can-close') } else { this.$refs.draftCloser.requestClose() diff --git a/src/components/post_status_form/post_status_form.scss b/src/components/post_status_form/post_status_form.scss new file mode 100644 index 000000000..9e8de25d5 --- /dev/null +++ b/src/components/post_status_form/post_status_form.scss @@ -0,0 +1,258 @@ +.post-status-form { + position: relative; + + .attachments { + margin-bottom: 0.5em; + } + + .more-post-actions { + height: 100%; + + .btn { + height: 100%; + } + } + + .form-bottom { + display: flex; + justify-content: space-between; + padding: 0.5em; + height: 2.5em; + + .post-button-group { + width: 10em; + display: flex; + + .post-button { + flex: 1 0 auto; + } + + .more-post-actions { + flex: 0 0 auto; + } + } + + p { + margin: 0.35em; + padding: 0.35em; + display: flex; + } + } + + .form-bottom-left { + display: flex; + flex: 1; + padding-right: 7px; + margin-right: 7px; + max-width: 10em; + } + + .preview-heading { + display: flex; + flex-wrap: wrap; + } + + .preview-toggle { + flex: 10 0 auto; + cursor: pointer; + user-select: none; + padding-left: 0.5em; + + &:hover { + text-decoration: underline; + } + + svg, + i { + margin-left: 0.2em; + font-size: 0.8em; + transform: rotate(90deg); + } + } + + .preview-container { + margin-bottom: 1em; + } + + .preview-error { + font-style: italic; + color: var(--textFaint); + } + + .preview-status { + border: 1px solid var(--border); + border-radius: var(--roundness); + padding: 0.5em; + margin: 0; + } + + .reply-or-quote-selector { + flex: 1 0 auto; + margin-bottom: 0.5em; + display: grid; + grid-template-columns: 1fr 1fr; + } + + .text-format { + .only-format { + color: var(--textFaint); + } + } + + .visibility-tray { + display: flex; + justify-content: space-between; + padding-top: 5px; + align-items: baseline; + } + + .visibility-notice.edit-warning { + > :first-child { + margin-top: 0; + } + + > :last-child { + margin-bottom: 0; + } + } + + // Order is not necessary but a good indicator + .media-upload-icon { + order: 1; + justify-content: left; + } + + .emoji-icon { + order: 2; + justify-content: center; + } + + .poll-icon { + order: 3; + justify-content: right; + } + + .media-upload-icon, + .poll-icon, + .emoji-icon { + font-size: 1.85em; + line-height: 1.1; + flex: 1; + padding: 0 0.1em; + display: flex; + align-items: center; + } + + .error { + text-align: center; + } + + .media-upload-wrapper { + margin-right: 0.2em; + margin-bottom: 0.5em; + width: 18em; + + img, + video { + object-fit: contain; + max-height: 10em; + } + + .video { + max-height: 10em; + } + + input { + flex: 1; + width: 100%; + } + } + + .status-input-wrapper { + display: flex; + position: relative; + width: 100%; + flex-direction: column; + } + + .btn[disabled] { + cursor: not-allowed; + } + + form { + display: flex; + flex-direction: column; + margin: 0.6em; + position: relative; + } + + .form-group { + display: flex; + flex-direction: column; + padding: 0.25em 0.5em 0.5em; + line-height: 1.85; + } + + .input.form-post-body { + // TODO: make a resizable textarea component? + box-sizing: content-box; // needed for easier computation of dynamic size + overflow: hidden; + transition: min-height 200ms 100ms; + // stock padding + 1 line of text (for counter) + padding-bottom: calc(var(--_padding) + var(--post-line-height) * 1em); + // two lines of text + height: calc(var(--post-line-height) * 1em); + min-height: calc(var(--post-line-height) * 1em); + resize: none; + background: transparent; + + &.scrollable-form { + overflow-y: auto; + } + } + + .main-input { + position: relative; + } + + .character-counter { + position: absolute; + bottom: 0; + right: 0; + padding: 0; + margin: 0 0.5em; + + &.error { + color: var(--cRed); + } + } + + @keyframes fade-in { + from { opacity: 0; } + to { opacity: 0.6; } + } + + @keyframes fade-out { + from { opacity: 0.6; } + to { opacity: 0; } + } + + .drop-indicator { + position: absolute; + width: 100%; + height: 100%; + font-size: 5em; + display: flex; + align-items: center; + justify-content: center; + opacity: 0.6; + color: var(--text); + background-color: var(--bg); + border-radius: var(--roundness); + border: 2px dashed var(--text); + } + + .auto-save-status { + align-self: center; + } +} diff --git a/src/components/post_status_form/post_status_form.vue b/src/components/post_status_form/post_status_form.vue index b7a169c58..8607b5a8f 100644 --- a/src/components/post_status_form/post_status_form.vue +++ b/src/components/post_status_form/post_status_form.vue @@ -264,6 +264,12 @@ :visible="pollFormVisible" :params="newStatus.poll" /> + + {{ autoSaveState }} +
- - {{ autoSaveState }} - - - - - +
+ + + + + +
- + diff --git a/src/i18n/en.json b/src/i18n/en.json index b4e56bc11..9f35d4342 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -1513,7 +1513,11 @@ }, "drafts": { "drafts": "Drafts", + "no_drafts": "You have no drafts", + "empty": "(No content)", + "poll_tooltip": "Draft contains a poll", "continue": "Continue composing", + "save": "Save without posting", "abandon": "Abandon draft", "abandon_confirm_title": "Abandon confirmation", "abandon_confirm": "Do you really want to abandon this draft?",