diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..ff4c2fd1b --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,13 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), + +## [Unreleased] +### Added +- Emoji picker +- Started changelog anew +### Changed +- changed the way fading effects for user profile/long statuses works, now uses css-mask instead of gradient background hacks which weren't exactly compatible with semi-transparent themes +### Fixed +- improved hotkey behavior on autocomplete popup diff --git a/docs/USER_GUIDE.md b/docs/USER_GUIDE.md index cb95a2440..18b6c5601 100644 --- a/docs/USER_GUIDE.md +++ b/docs/USER_GUIDE.md @@ -23,6 +23,15 @@ Posts will contain the text you are posting, but some content will be modified: **Depending on your instance some of the options might not be available or have different defaults** Let's clear up some basic stuff. When you post something it's called a **post** or it could be called a **status** or even a **toot** or a **prööt** depending on whom you ask. Post has body/content but it also has some other stuff in it - from attachments, visibility scope, subject line. +* **Emoji** are small images embedded in text, there are two major types of emoji: [unicode emoji](https://en.wikipedia.org/wiki/Emoji) and custom emoji. While unicode emoji are universal and standardized, they can appear differently depending on where you are using them or may not appear at all on older systems. Custom emoji are more *fun* kind - instance administrator can define many images as *custom emoji* for their users. This works very simple - custom emoji is defined by its *shortcode* and an image, so that any shortcode enclosed in colons get replaced with image if such shortcode exist. +Let's say there's `:pleroma:` emoji defined on instance. That means +> First time using :pleroma: pleroma! + +will become +> First time using ![pleroma](./example_emoji.png) pleroma! + +Note that you can only use emoji defined on your instance, you cannot "copy" someone else's emoji, and will have to ask your administrator to copy emoji from other instance to yours. +Lastly, there's two convenience options for emoji: an emoji picker (smiley face to the right of "submit" button) and autocomplete suggestions - when you start typing :shortcode: it will automatically try to suggest you emoj and complete the shortcode for you if you select one. **Note** that if emoji doesn't show up in suggestions nor in emoji picker it means there's no such emoji on your instance, if shortcode doesn't match any defined emoji it will appear as text. * **Attachments** are fairly simple - you can attach any file to a post as long as the file is within maximum size limits. If you're uploading explicit material you can mark all of your attachments as sensitive (or add `#nsfw` tag) - it will hide the images and videos behind a warning so that it won't be displayed instantly. * **Subject line** also known as **CW** (Content Warning) could be used as a header to the post and/or to warn others about contents of the post having something that might upset somebody or something among those lines. Several applications allow to hide post content leaving only subject line visible. As a side-effect using subject line will also mark your images as sensitive (see above). * **Visiblity scope** controls who will be able to see your posts. There are four scopes available: diff --git a/docs/example_emoji.png b/docs/example_emoji.png new file mode 100644 index 000000000..0a22a256b Binary files /dev/null and b/docs/example_emoji.png differ diff --git a/src/components/emoji_input/emoji_input.js b/src/components/emoji_input/emoji_input.js index 41ee239c5..25c21403c 100644 --- a/src/components/emoji_input/emoji_input.js +++ b/src/components/emoji_input/emoji_input.js @@ -64,7 +64,6 @@ const EmojiInput = { }, hideEmojiButton: { /** - enableStickerPicker: { * intended to use with external picker trigger, i.e. you have a button outside * input that will open up the picker, see triggerShowPicker() */ @@ -90,7 +89,7 @@ const EmojiInput = { blurTimeout: null, showPicker: false, temporarilyHideSuggestions: false, - spamMode: false, + keepOpen: false, disableClickOutside: false } }, @@ -98,6 +97,9 @@ const EmojiInput = { EmojiPicker }, computed: { + padEmoji () { + return this.$store.state.config.padEmoji + }, suggestions () { const firstchar = this.textAtCaret.charAt(0) if (this.textAtCaret === firstchar) { return [] } @@ -177,7 +179,7 @@ const EmojiInput = { this.$emit('input', newValue) this.caret = 0 }, - insert ({ insertion, spamMode }) { + insert ({ insertion, keepOpen }) { const before = this.value.substring(0, this.caret) || '' const after = this.value.substring(this.caret) || '' @@ -196,8 +198,8 @@ const EmojiInput = { * them, masto seem to be rendering :emoji::emoji: correctly now so why not */ const isSpaceRegex = /\s/ - const spaceBefore = !isSpaceRegex.exec(before.slice(-1)) && before.length && !spamMode > 0 ? ' ' : '' - const spaceAfter = !isSpaceRegex.exec(after[0]) && !spamMode ? ' ' : '' + const spaceBefore = !isSpaceRegex.exec(before.slice(-1)) && before.length && this.padEmoji > 0 ? ' ' : '' + const spaceAfter = !isSpaceRegex.exec(after[0]) && this.padEmoji ? ' ' : '' const newValue = [ before, @@ -206,13 +208,15 @@ const EmojiInput = { spaceAfter, after ].join('') - this.spamMode = spamMode + this.keepOpen = keepOpen this.$emit('input', newValue) const position = this.caret + (insertion + spaceAfter + spaceBefore).length + if (!keepOpen) { + this.input.elm.focus() + } this.$nextTick(function () { // Re-focus inputbox after clicking suggestion - this.input.elm.focus() // Set selection right after the replacement instead of the very end this.input.elm.setSelectionRange(position, position) this.caret = position @@ -284,7 +288,7 @@ const EmojiInput = { this.blurTimeout = null } - if (!this.spamMode) { + if (!this.keepOpen) { this.showPicker = false } this.focused = true diff --git a/src/components/emoji_picker/emoji_picker.js b/src/components/emoji_picker/emoji_picker.js index cb93f0c18..824412ddc 100644 --- a/src/components/emoji_picker/emoji_picker.js +++ b/src/components/emoji_picker/emoji_picker.js @@ -18,7 +18,7 @@ const EmojiPicker = { activeGroup: 'custom', showingStickers: false, groupsScrolledClass: 'scrolled-top', - spamMode: false + keepOpen: false } }, components: { @@ -27,7 +27,7 @@ const EmojiPicker = { methods: { onEmoji (emoji) { const value = emoji.imageUrl ? `:${emoji.displayText}:` : emoji.replacement - this.$emit('emoji', { insertion: value, spamMode: this.spamMode }) + this.$emit('emoji', { insertion: value, keepOpen: this.keepOpen }) }, highlight (key) { const ref = this.$refs['group-' + key] diff --git a/src/components/emoji_picker/emoji_picker.scss b/src/components/emoji_picker/emoji_picker.scss index 094388987..b0ed00e99 100644 --- a/src/components/emoji_picker/emoji_picker.scss +++ b/src/components/emoji_picker/emoji_picker.scss @@ -10,11 +10,11 @@ margin: 0 !important; z-index: 1; - .spam-mode { + .keep-open { padding: 7px; line-height: normal; } - .spam-mode-label { + .keep-open-label { padding: 0 7px; display: flex; } diff --git a/src/components/emoji_picker/emoji_picker.vue b/src/components/emoji_picker/emoji_picker.vue index b32d08622..6c43dd970 100644 --- a/src/components/emoji_picker/emoji_picker.vue +++ b/src/components/emoji_picker/emoji_picker.vue @@ -76,16 +76,16 @@
-
diff --git a/src/i18n/en.json b/src/i18n/en.json index 7676e7a83..20d4ed221 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -109,7 +109,7 @@ "emoji": { "stickers": "Stickers", "emoji": "Emoji", - "spam": "Keep picker open, don't separate emoji with spaces", + "keep_open": "Keep picker open", "search_emoji": "Search for an emoji", "add_emoji": "Insert emoji", "custom": "Custom emoji", @@ -232,6 +232,7 @@ "delete_account_error": "There was an issue deleting your account. If this persists please contact your instance administrator.", "delete_account_instructions": "Type your password in the input below to confirm account deletion.", "avatar_size_instruction": "The recommended minimum size for avatar images is 150x150 pixels.", + "pad_emoji": "Pad emoji with spaces when adding from picker", "export_theme": "Save preset", "filtering": "Filtering", "filtering_explanation": "All statuses containing these words will be muted, one per line", diff --git a/src/modules/config.js b/src/modules/config.js index 2bfad8f6f..cf04d14fe 100644 --- a/src/modules/config.js +++ b/src/modules/config.js @@ -7,6 +7,7 @@ const defaultState = { colors: {}, hideMutedPosts: undefined, // instance default collapseMessageWithSubject: undefined, // instance default + padEmoji: true, hideAttachments: false, hideAttachmentsInConv: false, maxThumbnails: 16, diff --git a/test/unit/specs/components/emoji_input.spec.js b/test/unit/specs/components/emoji_input.spec.js new file mode 100644 index 000000000..13a599615 --- /dev/null +++ b/test/unit/specs/components/emoji_input.spec.js @@ -0,0 +1,122 @@ +import { shallowMount, createLocalVue } from '@vue/test-utils' +import EmojiInput from 'src/components/emoji_input/emoji_input.vue' + +const generateInput = (value) => { + const localVue = createLocalVue() + localVue.directive('click-outside', () => {}) + const wrapper = shallowMount(EmojiInput, { + propsData: { + suggest: () => [], + enableEmojiPicker: true, + value + }, + slots: { + default: '' + }, + localVue + }) + return [wrapper, localVue] +} + +describe('EmojiInput', () => { + describe('insertion mechanism', () => { + it('inserts string at the end with trailing space', () => { + const initialString = 'Testing' + const [wrapper] = generateInput(initialString) + const input = wrapper.find('input') + input.setValue(initialString) + wrapper.setData({ caret: initialString.length }) + wrapper.vm.insert({ insertion: '(test)', keepOpen: false }) + expect(wrapper.emitted().input[0][0]).to.eql('Testing (test) ') + }) + + it('inserts string at the end with trailing space (source has a trailing space)', () => { + const initialString = 'Testing ' + const [wrapper] = generateInput(initialString) + const input = wrapper.find('input') + input.setValue(initialString) + wrapper.setData({ caret: initialString.length }) + wrapper.vm.insert({ insertion: '(test)', keepOpen: false }) + expect(wrapper.emitted().input[0][0]).to.eql('Testing (test) ') + }) + + it('inserts string at the begginning without leading space', () => { + const initialString = 'Testing' + const [wrapper] = generateInput(initialString) + const input = wrapper.find('input') + input.setValue(initialString) + wrapper.setData({ caret: 0 }) + wrapper.vm.insert({ insertion: '(test)', keepOpen: false }) + expect(wrapper.emitted().input[0][0]).to.eql('(test) Testing') + }) + + it('inserts string between words without creating extra spaces', () => { + const initialString = 'Spurdo Sparde' + const [wrapper] = generateInput(initialString) + const input = wrapper.find('input') + input.setValue(initialString) + wrapper.setData({ caret: 6 }) + wrapper.vm.insert({ insertion: ':ebin:', keepOpen: false }) + expect(wrapper.emitted().input[0][0]).to.eql('Spurdo :ebin: Sparde') + }) + + it('inserts string between words without creating extra spaces (other caret)', () => { + const initialString = 'Spurdo Sparde' + const [wrapper] = generateInput(initialString) + const input = wrapper.find('input') + input.setValue(initialString) + wrapper.setData({ caret: 7 }) + wrapper.vm.insert({ insertion: ':ebin:', keepOpen: false }) + expect(wrapper.emitted().input[0][0]).to.eql('Spurdo :ebin: Sparde') + }) + + it('inserts string without any padding in spam mode', () => { + const initialString = 'Eat some spam!' + const [wrapper] = generateInput(initialString) + const input = wrapper.find('input') + input.setValue(initialString) + wrapper.setData({ caret: initialString.length }) + wrapper.vm.insert({ insertion: ':spam:', keepOpen: true }) + expect(wrapper.emitted().input[0][0]).to.eql('Eat some spam!:spam:') + }) + + it('correctly sets caret after insertion at beginning', (done) => { + const initialString = '1234' + const [wrapper, vue] = generateInput(initialString) + const input = wrapper.find('input') + input.setValue(initialString) + wrapper.setData({ caret: 0 }) + wrapper.vm.insert({ insertion: '1234', keepOpen: false }) + vue.nextTick(() => { + expect(wrapper.vm.caret).to.eql(5) + done() + }) + }) + + it('correctly sets caret after insertion at end', (done) => { + const initialString = '1234' + const [wrapper, vue] = generateInput(initialString) + const input = wrapper.find('input') + input.setValue(initialString) + wrapper.setData({ caret: initialString.length }) + wrapper.vm.insert({ insertion: '1234', keepOpen: false }) + vue.nextTick(() => { + expect(wrapper.vm.caret).to.eql(10) + done() + }) + }) + + it('correctly sets caret after insertion in spam mode', (done) => { + const initialString = '1234' + const [wrapper, vue] = generateInput(initialString) + const input = wrapper.find('input') + input.setValue(initialString) + wrapper.setData({ caret: initialString.length }) + wrapper.vm.insert({ insertion: '1234', keepOpen: true }) + vue.nextTick(() => { + expect(wrapper.vm.caret).to.eql(8) + done() + }) + }) + }) +})