From d695dcaff91ce616549dc54c02b8bf9167f2be58 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Fri, 9 Apr 2021 19:09:22 +0300 Subject: experimental flash support through ruffle --- src/services/file_type/file_type.service.js | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'src/services') diff --git a/src/services/file_type/file_type.service.js b/src/services/file_type/file_type.service.js index 2a046bec..5182ecd1 100644 --- a/src/services/file_type/file_type.service.js +++ b/src/services/file_type/file_type.service.js @@ -2,6 +2,10 @@ // or the entire service could be just mimetype service that only operates // on mimetypes and not files. Currently the naming is confusing. const fileType = mimetype => { + if (mimetype.match(/flash/)) { + return 'flash' + } + if (mimetype.match(/text\/html/)) { return 'html' } -- cgit v1.2.3-70-g09d2 From 5fdc4a190462272f678ee313dee049493cf44f76 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Mon, 12 Apr 2021 00:02:46 +0300 Subject: whooops dropped my monstercondo --- src/services/ruffle_service/ruffle_service.js | 37 +++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 src/services/ruffle_service/ruffle_service.js (limited to 'src/services') diff --git a/src/services/ruffle_service/ruffle_service.js b/src/services/ruffle_service/ruffle_service.js new file mode 100644 index 00000000..61fe1e19 --- /dev/null +++ b/src/services/ruffle_service/ruffle_service.js @@ -0,0 +1,37 @@ +const createRuffleService = () => { + let ruffleInstance = null + + const getRuffle = () => new Promise((resolve, reject) => { + if (ruffleInstance) resolve(ruffleInstance) + // Ruffle needs these to be set before it's loaded + // https://github.com/ruffle-rs/ruffle/issues/3952 + window.RufflePlayer = {} + window.RufflePlayer.config = { + polyfills: false, + publicPath: '/static/ruffle' + } + + // Currently it's seems like a better way of loading ruffle + // because it needs the wasm publically accessible, but it needs path to it + // and filename of wasm seems to be pseudo-randomly generated (is it a hash?) + const script = document.createElement('script') + // see webpack config, using CopyPlugin to copy it from node_modules + // provided via ruffle-mirror + script.src = '/static/ruffle/ruffle.js' + script.type = 'text/javascript' + script.onerror = (e) => { reject(e) } + script.onabort = (e) => { reject(e) } + script.oncancel = (e) => { reject(e) } + script.onload = () => { + ruffleInstance = window.RufflePlayer + resolve(ruffleInstance) + } + document.body.appendChild(script) + }) + + return { getRuffle } +} + +const RuffleService = createRuffleService() + +export default RuffleService -- cgit v1.2.3-70-g09d2 From 87903fbf6dd8fbc47fe5a49b901aa295e07529e8 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Mon, 12 Apr 2021 01:15:59 +0300 Subject: do not load ruffle multiple times! --- src/services/ruffle_service/ruffle_service.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'src/services') diff --git a/src/services/ruffle_service/ruffle_service.js b/src/services/ruffle_service/ruffle_service.js index 61fe1e19..7411dd96 100644 --- a/src/services/ruffle_service/ruffle_service.js +++ b/src/services/ruffle_service/ruffle_service.js @@ -2,7 +2,10 @@ const createRuffleService = () => { let ruffleInstance = null const getRuffle = () => new Promise((resolve, reject) => { - if (ruffleInstance) resolve(ruffleInstance) + if (ruffleInstance) { + resolve(ruffleInstance) + return + } // Ruffle needs these to be set before it's loaded // https://github.com/ruffle-rs/ruffle/issues/3952 window.RufflePlayer = {} -- cgit v1.2.3-70-g09d2 From 06d0254cc55f3a5a3c4e2150af05675bd153829a Mon Sep 17 00:00:00 2001 From: Matilde Park Date: Wed, 21 Apr 2021 01:40:25 -0400 Subject: entity_normalizer: safely check screen_name Prevents a crash on undefined screen name cases. --- src/services/entity_normalizer/entity_normalizer.service.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/services') diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js index 9b2b30e6..a4ddf927 100644 --- a/src/services/entity_normalizer/entity_normalizer.service.js +++ b/src/services/entity_normalizer/entity_normalizer.service.js @@ -205,7 +205,7 @@ export const parseUser = (data) => { // Convert punycode to unicode for UI output.screen_name_ui = output.screen_name - if (output.screen_name.includes('@')) { + if (output.screen_name && output.screen_name.includes('@')) { const parts = output.screen_name.split('@') let unicodeDomain = punycode.toUnicode(parts[1]) if (unicodeDomain !== parts[1]) { -- cgit v1.2.3-70-g09d2 From 008e711e116044f76a13e35cfd616fc211a3d6f0 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Wed, 2 Jun 2021 12:15:31 +0300 Subject: fix favico badge not working on chrome --- src/services/favicon_service/favicon_service.js | 1 + 1 file changed, 1 insertion(+) (limited to 'src/services') diff --git a/src/services/favicon_service/favicon_service.js b/src/services/favicon_service/favicon_service.js index d1ddee41..e78aa2ae 100644 --- a/src/services/favicon_service/favicon_service.js +++ b/src/services/favicon_service/favicon_service.js @@ -14,6 +14,7 @@ const createFaviconService = () => { favcanvas.width = faviconWidth favcanvas.height = faviconHeight favimg = new Image() + favimg.crossOrigin = 'anonymous' favimg.src = favicon.href favcontext = favcanvas.getContext('2d') } -- cgit v1.2.3-70-g09d2 From 2a2483f4c9db7142676fb9be1e2917f007301e7f Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Wed, 2 Jun 2021 12:47:54 +0300 Subject: handle multiple favicons (different sizes) --- src/services/favicon_service/favicon_service.js | 71 +++++++++++++------------ 1 file changed, 38 insertions(+), 33 deletions(-) (limited to 'src/services') diff --git a/src/services/favicon_service/favicon_service.js b/src/services/favicon_service/favicon_service.js index e78aa2ae..7e19629d 100644 --- a/src/services/favicon_service/favicon_service.js +++ b/src/services/favicon_service/favicon_service.js @@ -1,53 +1,58 @@ -import { find } from 'lodash' - const createFaviconService = () => { - let favimg, favcanvas, favcontext, favicon + const favicons = [] const faviconWidth = 128 const faviconHeight = 128 const badgeRadius = 32 const initFaviconService = () => { - const nodes = document.getElementsByTagName('link') - favicon = find(nodes, node => node.rel === 'icon') - if (favicon) { - favcanvas = document.createElement('canvas') - favcanvas.width = faviconWidth - favcanvas.height = faviconHeight - favimg = new Image() - favimg.crossOrigin = 'anonymous' - favimg.src = favicon.href - favcontext = favcanvas.getContext('2d') - } + const nodes = document.querySelectorAll('link[rel="icon"]') + nodes.forEach(favicon => { + if (favicon) { + const favcanvas = document.createElement('canvas') + favcanvas.width = faviconWidth + favcanvas.height = faviconHeight + const favimg = new Image() + favimg.crossOrigin = 'anonymous' + favimg.src = favicon.href + const favcontext = favcanvas.getContext('2d') + favicons.push({ favcanvas, favimg, favcontext, favicon }) + } + }) } const isImageLoaded = (img) => img.complete && img.naturalHeight !== 0 const clearFaviconBadge = () => { - if (!favimg || !favcontext || !favicon) return + if (favicons.length === 0) return + favicons.forEach(({ favimg, favcanvas, favcontext, favicon }) => { + if (!favimg || !favcontext || !favicon) return - favcontext.clearRect(0, 0, faviconWidth, faviconHeight) - if (isImageLoaded(favimg)) { - favcontext.drawImage(favimg, 0, 0, favimg.width, favimg.height, 0, 0, faviconWidth, faviconHeight) - } - favicon.href = favcanvas.toDataURL('image/png') + favcontext.clearRect(0, 0, faviconWidth, faviconHeight) + if (isImageLoaded(favimg)) { + favcontext.drawImage(favimg, 0, 0, favimg.width, favimg.height, 0, 0, faviconWidth, faviconHeight) + } + favicon.href = favcanvas.toDataURL('image/png') + }) } const drawFaviconBadge = () => { - if (!favimg || !favcontext || !favcontext) return - + if (favicons.length === 0) return clearFaviconBadge() + favicons.forEach(({ favimg, favcanvas, favcontext, favicon }) => { + if (!favimg || !favcontext || !favcontext) return + + const style = getComputedStyle(document.body) + const badgeColor = `${style.getPropertyValue('--badgeNotification') || 'rgb(240, 100, 100)'}` - const style = getComputedStyle(document.body) - const badgeColor = `${style.getPropertyValue('--badgeNotification') || 'rgb(240, 100, 100)'}` - - if (isImageLoaded(favimg)) { - favcontext.drawImage(favimg, 0, 0, favimg.width, favimg.height, 0, 0, faviconWidth, faviconHeight) - } - favcontext.fillStyle = badgeColor - favcontext.beginPath() - favcontext.arc(faviconWidth - badgeRadius, badgeRadius, badgeRadius, 0, 2 * Math.PI, false) - favcontext.fill() - favicon.href = favcanvas.toDataURL('image/png') + if (isImageLoaded(favimg)) { + favcontext.drawImage(favimg, 0, 0, favimg.width, favimg.height, 0, 0, faviconWidth, faviconHeight) + } + favcontext.fillStyle = badgeColor + favcontext.beginPath() + favcontext.arc(faviconWidth - badgeRadius, badgeRadius, badgeRadius, 0, 2 * Math.PI, false) + favcontext.fill() + favicon.href = favcanvas.toDataURL('image/png') + }) } return { -- cgit v1.2.3-70-g09d2 From 20ce6468520e76b0fb2931a5fac368157d950b1d Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Mon, 7 Jun 2021 03:14:48 +0300 Subject: [WIP] MUCH better approach to replacing emojis with still versions --- .babelrc | 4 +- package.json | 4 +- src/components/rich_content/rich_content.jsx | 66 ++++++++++ src/components/rich_content/rich_content.scss | 0 src/components/status_content/status_content.js | 6 +- src/components/status_content/status_content.vue | 14 +-- .../entity_normalizer/entity_normalizer.service.js | 3 + .../mini_html_converter.service.js | 137 +++++++++++++++++++++ .../mini_post_html_processor.spec.js | 130 +++++++++++++++++++ 9 files changed, 350 insertions(+), 14 deletions(-) create mode 100644 src/components/rich_content/rich_content.jsx create mode 100644 src/components/rich_content/rich_content.scss create mode 100644 src/services/mini_html_converter/mini_html_converter.service.js create mode 100644 test/unit/specs/services/tiny_post_html_processor/mini_post_html_processor.spec.js (limited to 'src/services') diff --git a/.babelrc b/.babelrc index 3c732dd1..94521147 100644 --- a/.babelrc +++ b/.babelrc @@ -1,5 +1,5 @@ { - "presets": ["@babel/preset-env"], - "plugins": ["@babel/plugin-transform-runtime", "lodash", "@vue/babel-plugin-transform-vue-jsx"], + "presets": ["@babel/preset-env", "@vue/babel-preset-jsx"], + "plugins": ["@babel/plugin-transform-runtime", "lodash"], "comments": false } diff --git a/package.json b/package.json index 99301266..5134a8b1 100644 --- a/package.json +++ b/package.json @@ -47,8 +47,8 @@ "@babel/preset-env": "^7.7.6", "@babel/register": "^7.7.4", "@ungap/event-target": "^0.1.0", - "@vue/babel-helper-vue-jsx-merge-props": "^1.0.0", - "@vue/babel-plugin-transform-vue-jsx": "^1.1.2", + "@vue/babel-helper-vue-jsx-merge-props": "^1.2.1", + "@vue/babel-preset-jsx": "^1.2.4", "@vue/test-utils": "^1.0.0-beta.26", "autoprefixer": "^6.4.0", "babel-eslint": "^7.0.0", diff --git a/src/components/rich_content/rich_content.jsx b/src/components/rich_content/rich_content.jsx new file mode 100644 index 00000000..3b29eb4c --- /dev/null +++ b/src/components/rich_content/rich_content.jsx @@ -0,0 +1,66 @@ +import Vue from 'vue' +import { mapGetters } from 'vuex' +import { processHtml } from 'src/services/tiny_post_html_processor/tiny_post_html_processor.service.js' +import { convertHtml, getTagName, processTextForEmoji, getAttrs } from 'src/services/mini_html_converter/mini_html_converter.service.js' +import { mentionMatchesUrl, extractTagFromUrl } from 'src/services/matcher/matcher.service.js' +import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' +import StillImage from 'src/components/still-image/still-image.vue' + +import './rich_content.scss' + +export default Vue.component('RichContent', { + name: 'RichContent', + props: { + html: { + required: true, + type: String + }, + emoji: { + required: true, + type: Array + } + }, + render (h) { + const renderImage = (tag) => { + return + } + const structure = convertHtml(this.html) + const processItem = (item) => { + if (typeof item === 'string') { + if (item.includes(':')) { + return processTextForEmoji( + item, + this.emoji, + ({ shortcode, url }) => { + return + } + ) + } else { + return item + } + } + if (Array.isArray(item)) { + const [opener, children] = item + const Tag = getTagName(opener) + if (Tag === 'img') { + return renderImage(opener) + } + if (children !== undefined) { + return + { children.map(processItem) } + + } else { + return + } + } + } + return
+ { structure.map(processItem) } +
+ } +}) diff --git a/src/components/rich_content/rich_content.scss b/src/components/rich_content/rich_content.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/components/status_content/status_content.js b/src/components/status_content/status_content.js index a6f79d76..571f1a78 100644 --- a/src/components/status_content/status_content.js +++ b/src/components/status_content/status_content.js @@ -1,6 +1,7 @@ import Attachment from '../attachment/attachment.vue' import Poll from '../poll/poll.vue' import Gallery from '../gallery/gallery.vue' +import RichContent from 'src/components/rich_content/rich_content.jsx' import LinkPreview from '../link-preview/link-preview.vue' import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' import fileType from 'src/services/file_type/file_type.service' @@ -125,7 +126,7 @@ const StatusContent = { return this.mergedConfig.maxThumbnails }, postBodyHtml () { - const html = this.status.statusnet_html + const html = this.status.raw_html if (this.mergedConfig.greentext) { try { @@ -164,7 +165,8 @@ const StatusContent = { Attachment, Poll, Gallery, - LinkPreview + LinkPreview, + RichContent }, methods: { linkClicked (event) { diff --git a/src/components/status_content/status_content.vue b/src/components/status_content/status_content.vue index 90bfaf40..c1a78db9 100644 --- a/src/components/status_content/status_content.vue +++ b/src/components/status_content/status_content.vue @@ -1,5 +1,4 @@ diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js index a4ddf927..9f63feb6 100644 --- a/src/services/entity_normalizer/entity_normalizer.service.js +++ b/src/services/entity_normalizer/entity_normalizer.service.js @@ -267,6 +267,8 @@ export const parseStatus = (data) => { output.nsfw = data.sensitive output.statusnet_html = addEmojis(data.content, data.emojis) + output.raw_html = data.content + output.emojis = data.emojis output.tags = data.tags @@ -293,6 +295,7 @@ export const parseStatus = (data) => { output.retweeted_status = parseStatus(data.reblog) } + output.summary_raw_html = escape(data.spoiler_text) output.summary_html = addEmojis(escape(data.spoiler_text), data.emojis) output.external_url = data.url output.poll = data.poll diff --git a/src/services/mini_html_converter/mini_html_converter.service.js b/src/services/mini_html_converter/mini_html_converter.service.js new file mode 100644 index 00000000..00d20019 --- /dev/null +++ b/src/services/mini_html_converter/mini_html_converter.service.js @@ -0,0 +1,137 @@ +/** + * This is a not-so-tiny purpose-built HTML parser/processor. It was made for use + * with StatusText component for purpose of replacing tags with vue components + * + * known issue: doesn't handle CDATA so nested CDATA might not work well + * + * @param {Object} input - input data + * @param {(string) => string} lineProcessor - function that will be called on every line + * @param {{ key[string]: (string) => string}} tagProcessor - map of processors for tags + * @return {string} processed html + */ +export const convertHtml = (html) => { + // Elements that are implicitly self-closing + // https://developer.mozilla.org/en-US/docs/Glossary/empty_element + const emptyElements = new Set([ + 'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', + 'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr' + ]) + // TODO For future - also parse HTML5 multi-source components? + + const buffer = [] // Current output buffer + const levels = [['', buffer]] // How deep we are in tags and which tags were there + let textBuffer = '' // Current line content + let tagBuffer = null // Current tag buffer, if null = we are not currently reading a tag + + const getCurrentBuffer = () => { + return levels[levels.length - 1][1] + } + + const flushText = () => { // Processes current line buffer, adds it to output buffer and clears line buffer + if (textBuffer === '') return + getCurrentBuffer().push(textBuffer) + textBuffer = '' + } + + const handleSelfClosing = (tag) => { + getCurrentBuffer().push([tag]) + } + + const handleOpen = (tag) => { + const curBuf = getCurrentBuffer() + const newLevel = [tag, []] + levels.push(newLevel) + curBuf.push(newLevel) + } + + const handleClose = (tag) => { + const currentTag = levels[levels.length - 1] + if (getTagName(levels[levels.length - 1][0]) === getTagName(tag)) { + currentTag.push(tag) + levels.pop() + } else { + getCurrentBuffer().push(tag) + } + } + + for (let i = 0; i < html.length; i++) { + const char = html[i] + if (char === '<' && tagBuffer === null) { + flushText() + tagBuffer = char + } else if (char !== '>' && tagBuffer !== null) { + tagBuffer += char + } else if (char === '>' && tagBuffer !== null) { + tagBuffer += char + const tagFull = tagBuffer + tagBuffer = null + const tagName = getTagName(tagFull) + if (tagFull[1] === '/') { + handleClose(tagFull) + } else if (emptyElements.has(tagName) || tagFull[tagFull.length - 2] === '/') { + // self-closing + handleSelfClosing(tagFull) + } else { + handleOpen(tagFull) + } + } else { + textBuffer += char + } + } + if (tagBuffer) { + textBuffer += tagBuffer + } + + flushText() + return buffer +} + +// Extracts tag name from tag, i.e. => span +export const getTagName = (tag) => { + const result = /(?:<\/(\w+)>|<(\w+)\s?.*?\/?>)/gi.exec(tag) + return result && (result[1] || result[2]) +} + +export const processTextForEmoji = (text, emojis, processor) => { + const buffer = [] + let textBuffer = '' + for (let i = 0; i < text.length; i++) { + const char = text[i] + if (char === ':') { + const next = text.slice(i + 1) + let found = false + for (let emoji of emojis) { + if (next.slice(0, emoji.shortcode.length + 1) === (emoji.shortcode + ':')) { + found = emoji + break + } + } + if (found) { + buffer.push(textBuffer) + textBuffer = '' + buffer.push(processor(found)) + i += found.shortcode.length + 1 + } else { + textBuffer += char + } + } else { + textBuffer += char + } + } + return buffer +} + +export const getAttrs = tag => { + const innertag = tag + .substring(1, tag.length - 1) + .replace(new RegExp('^' + getTagName(tag)), '') + .replace(/\/?$/, '') + .trim() + const attrs = Array.from(innertag.matchAll(/([a-z0-9-]+)(?:=(?:"([^"]+?)"|'([^']+?)'))?/gi)) + .map(([trash, key, value]) => [key, value]) + .map(([k, v]) => { + if (!v) return [k, true] + return [k, v] + }) + return Object.fromEntries(attrs) +} diff --git a/test/unit/specs/services/tiny_post_html_processor/mini_post_html_processor.spec.js b/test/unit/specs/services/tiny_post_html_processor/mini_post_html_processor.spec.js new file mode 100644 index 00000000..41818f57 --- /dev/null +++ b/test/unit/specs/services/tiny_post_html_processor/mini_post_html_processor.spec.js @@ -0,0 +1,130 @@ +import { convertHtml, processTextForEmoji } from 'src/services/mini_html_converter/mini_html_converter.service.js' + +describe('MiniHtmlConverter', () => { + describe('convertHtml', () => { + it('converts html into a tree structure', () => { + const inputOutput = '1

2

345' + expect(convertHtml(inputOutput)).to.eql([ + '1 ', + [ + '

', + ['2'], + '

' + ], + ' ', + [ + '', + [ + '3', + [''], + '4' + ], + '' + ], + '5' + ]) + }) + it('converts html to tree while preserving tag formatting', () => { + const inputOutput = '1

2

345' + expect(convertHtml(inputOutput)).to.eql([ + '1 ', + [ + '

', + ['2'], + '

' + ], + [ + '', + [ + '3', + [''], + '4' + ], + '' + ], + '5' + ]) + }) + it('converts semi-broken html', () => { + const inputOutput = '1
2

42' + expect(convertHtml(inputOutput)).to.eql([ + '1 ', + ['
'], + ' 2 ', + [ + '

', + [' 42'] + ] + ]) + }) + it('realistic case', () => { + const inputOutput = '

@benis @hj nice

' + expect(convertHtml(inputOutput)).to.eql([ + [ + '

', + [ + [ + '', + [ + [ + '', + [ + '@', + [ + '', + [ + 'benis' + ], + '' + ] + ], + '' + ] + ], + '' + ], + ' ', + [ + '', + [ + [ + '', + [ + '@', + [ + '', + [ + 'hj' + ], + '' + ] + ], + '' + ] + ], + '' + ], + ' nice' + ], + '

' + ] + ]) + }) + }) + describe('processTextForEmoji', () => { + it('processes all emoji in text', () => { + const inputOutput = 'Hello from finland! :lol: We have best water! :lmao:' + const emojis = [ + { shortcode: 'lol', src: 'LOL' }, + { shortcode: 'lmao', src: 'LMAO' } + ] + const processor = ({ shortcode, src }) => ({ shortcode, src }) + expect(processTextForEmoji(inputOutput, emojis, processor)).to.eql([ + 'Hello from finland! ', + { shortcode: 'lol', src: 'LOL' }, + ' We have best water! ', + { shortcode: 'lmao', src: 'LMAO' } + ]) + }) + }) +}) -- cgit v1.2.3-70-g09d2 From be79643bcf6f05df500f0d6bcb773f07dd15da3d Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Mon, 7 Jun 2021 12:38:27 +0300 Subject: fix emoji processor not leaving string as-is if no emoji are found --- .../mini_html_converter.service.js | 1 + .../mini_post_html_processor.spec.js | 29 +++++++++++++++++++++- 2 files changed, 29 insertions(+), 1 deletion(-) (limited to 'src/services') diff --git a/src/services/mini_html_converter/mini_html_converter.service.js b/src/services/mini_html_converter/mini_html_converter.service.js index 00d20019..879ff544 100644 --- a/src/services/mini_html_converter/mini_html_converter.service.js +++ b/src/services/mini_html_converter/mini_html_converter.service.js @@ -118,6 +118,7 @@ export const processTextForEmoji = (text, emojis, processor) => { textBuffer += char } } + if (textBuffer) buffer.push(textBuffer) return buffer } diff --git a/test/unit/specs/services/tiny_post_html_processor/mini_post_html_processor.spec.js b/test/unit/specs/services/tiny_post_html_processor/mini_post_html_processor.spec.js index 41818f57..c4e3f688 100644 --- a/test/unit/specs/services/tiny_post_html_processor/mini_post_html_processor.spec.js +++ b/test/unit/specs/services/tiny_post_html_processor/mini_post_html_processor.spec.js @@ -57,7 +57,7 @@ describe('MiniHtmlConverter', () => { ] ]) }) - it('realistic case', () => { + it('realistic case 1', () => { const inputOutput = '

@benis @hj nice

' expect(convertHtml(inputOutput)).to.eql([ [ @@ -110,6 +110,24 @@ describe('MiniHtmlConverter', () => { ] ]) }) + it('realistic case 2', () => { + const inputOutput = 'Country improv: give me a city
Audience: Memphis
Improv troupe: come on, a better one
Audience: el paso' + expect(convertHtml(inputOutput)).to.eql([ + 'Country improv: give me a city', + [ + '
' + ], + 'Audience: Memphis', + [ + '
' + ], + 'Improv troupe: come on, a better one', + [ + '
' + ], + 'Audience: el paso' + ]) + }) }) describe('processTextForEmoji', () => { it('processes all emoji in text', () => { @@ -126,5 +144,14 @@ describe('MiniHtmlConverter', () => { { shortcode: 'lmao', src: 'LMAO' } ]) }) + it('leaves text as is', () => { + const inputOutput = 'Number one: that\'s terror' + const emojis = [] + const processor = ({ shortcode, src }) => ({ shortcode, src }) + expect(processTextForEmoji(inputOutput, emojis, processor)).to.eql([ + 'Number one: that\'s terror' + ]) + }) + }) }) }) -- cgit v1.2.3-70-g09d2 From b0ae32e309134f0e91026c6712f2e9081f493c22 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Mon, 7 Jun 2021 16:31:39 +0300 Subject: made getAttrs correctly handle both ' and " --- src/services/mini_html_converter/mini_html_converter.service.js | 4 ++-- .../tiny_post_html_processor/mini_post_html_processor.spec.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) (limited to 'src/services') diff --git a/src/services/mini_html_converter/mini_html_converter.service.js b/src/services/mini_html_converter/mini_html_converter.service.js index 879ff544..01f8adf8 100644 --- a/src/services/mini_html_converter/mini_html_converter.service.js +++ b/src/services/mini_html_converter/mini_html_converter.service.js @@ -128,11 +128,11 @@ export const getAttrs = tag => { .replace(new RegExp('^' + getTagName(tag)), '') .replace(/\/?$/, '') .trim() - const attrs = Array.from(innertag.matchAll(/([a-z0-9-]+)(?:=(?:"([^"]+?)"|'([^']+?)'))?/gi)) + const attrs = Array.from(innertag.matchAll(/([a-z0-9-]+)(?:=("[^"]+?"|'[^']+?'))?/gi)) .map(([trash, key, value]) => [key, value]) .map(([k, v]) => { if (!v) return [k, true] - return [k, v] + return [k, v.substring(1, v.length - 1)] }) return Object.fromEntries(attrs) } diff --git a/test/unit/specs/services/tiny_post_html_processor/mini_post_html_processor.spec.js b/test/unit/specs/services/tiny_post_html_processor/mini_post_html_processor.spec.js index b42f5f35..8df2fbc3 100644 --- a/test/unit/specs/services/tiny_post_html_processor/mini_post_html_processor.spec.js +++ b/test/unit/specs/services/tiny_post_html_processor/mini_post_html_processor.spec.js @@ -157,7 +157,7 @@ describe('MiniHtmlConverter', () => { describe('getAttrs', () => { it('extracts arguments from tag', () => { - const input = '' + const input = '' const output = { src: 'boop', cool: true, ebin: 'true' } expect(getAttrs(input)).to.eql(output) -- cgit v1.2.3-70-g09d2 From 04fa1f0b2d1a92b1c653cd55f51ee7e1434b2bd7 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Mon, 7 Jun 2021 18:39:51 +0300 Subject: some docs, added richcontent to usernames in status, updated stillImage to allow scale of "gif" label --- src/components/rich_content/rich_content.jsx | 33 +++++++++++++++++----- src/components/rich_content/rich_content.scss | 4 +-- src/components/status/status.js | 7 +++-- src/components/status/status.scss | 8 ++---- src/components/status/status.vue | 12 ++++---- src/components/still-image/still-image.vue | 3 +- .../entity_normalizer/entity_normalizer.service.js | 1 + 7 files changed, 44 insertions(+), 24 deletions(-) (limited to 'src/services') diff --git a/src/components/rich_content/rich_content.jsx b/src/components/rich_content/rich_content.jsx index 00cf6623..c15877c8 100644 --- a/src/components/rich_content/rich_content.jsx +++ b/src/components/rich_content/rich_content.jsx @@ -9,25 +9,41 @@ import './rich_content.scss' export default Vue.component('RichContent', { name: 'RichContent', props: { + // Original html content html: { required: true, type: String }, + // Emoji object, as in status.emojis, note the "s" at the end... emoji: { required: true, type: Array + }, + // Whether to handle links or not (posts: yes, everything else: no) + handleLinks: { + required: false, + type: Boolean, + default: false } }, render (h) { const renderImage = (tag) => { - const attrs = getAttrs(tag) - return + return } const renderMention = (attrs, children) => { - return + return } - const structure = convertHtml(this.html) + + // Processor to use with mini_html_converter const processItem = (item) => { + // Handle text noes - just add emoji if (typeof item === 'string') { if (item.includes(':')) { return processTextForEmoji( @@ -46,18 +62,21 @@ export default Vue.component('RichContent', { return unescape(item) } } + // Handle tag nodes if (Array.isArray(item)) { const [opener, children] = item const Tag = getTagName(opener) switch (Tag) { - case 'img': + case 'img': // replace images with StillImage return renderImage(opener) - case 'a': + case 'a': // replace mentions with MentionLink + if (!this.handleLinks) break const attrs = getAttrs(opener) if (attrs['class'] && attrs['class'].includes('mention')) { return renderMention(attrs, children) } } + // Render tag as is if (children !== undefined) { return { children.map(processItem) } @@ -68,7 +87,7 @@ export default Vue.component('RichContent', { } } return - { structure.map(processItem) } + { convertHtml(this.html).map(processItem) } } }) diff --git a/src/components/rich_content/rich_content.scss b/src/components/rich_content/rich_content.scss index 2fcd3911..a486babf 100644 --- a/src/components/rich_content/rich_content.scss +++ b/src/components/rich_content/rich_content.scss @@ -56,8 +56,8 @@ } .emoji { - width: 32px; - height: 32px; + width: var(--emoji-size, 32px); + height: var(--emoji-size, 32px); } .img, diff --git a/src/components/status/status.js b/src/components/status/status.js index 470c01f1..b6414ad8 100644 --- a/src/components/status/status.js +++ b/src/components/status/status.js @@ -9,6 +9,7 @@ import UserAvatar from '../user_avatar/user_avatar.vue' import AvatarList from '../avatar_list/avatar_list.vue' import Timeago from '../timeago/timeago.vue' import StatusContent from '../status_content/status_content.vue' +import RichContent from 'src/components/rich_content/rich_content.jsx' import StatusPopover from '../status_popover/status_popover.vue' import UserListPopover from '../user_list_popover/user_list_popover.vue' import EmojiReactions from '../emoji_reactions/emoji_reactions.vue' @@ -68,7 +69,8 @@ const Status = { StatusPopover, UserListPopover, EmojiReactions, - StatusContent + StatusContent, + RichContent }, props: [ 'statusoid', @@ -136,8 +138,9 @@ const Status = { } }, retweet () { return !!this.statusoid.retweeted_status }, + retweeterUser () { return this.statusoid.user }, retweeter () { return this.statusoid.user.name || this.statusoid.user.screen_name_ui }, - retweeterHtml () { return this.statusoid.user.name_html }, + retweeterHtml () { return this.statusoid.user.name }, retweeterProfileLink () { return this.generateUserProfileLink(this.statusoid.user.id, this.statusoid.user.screen_name) }, status () { if (this.retweet) { diff --git a/src/components/status/status.scss b/src/components/status/status.scss index 58b55bc8..82088943 100644 --- a/src/components/status/status.scss +++ b/src/components/status/status.scss @@ -93,12 +93,8 @@ $status-margin: 0.75em; margin-right: 0.4em; text-overflow: ellipsis; - .emoji { - width: 14px; - height: 14px; - vertical-align: middle; - object-fit: contain; - } + --_still_image-label-scale: 0.25; + --emoji-size: 14px; } .status-favicon { diff --git a/src/components/status/status.vue b/src/components/status/status.vue index 00e962f3..cc5fb79f 100644 --- a/src/components/status/status.vue +++ b/src/components/status/status.vue @@ -1,5 +1,4 @@ - diff --git a/src/components/still-image/still-image.vue b/src/components/still-image/still-image.vue index d3eb5925..a3610b51 100644 --- a/src/components/still-image/still-image.vue +++ b/src/components/still-image/still-image.vue @@ -30,7 +30,7 @@ position: relative; line-height: 0; overflow: hidden; - display: flex; + display: inline-flex; align-items: center; canvas { @@ -53,6 +53,7 @@ &.animated { &::before { + zoom: var(--_still_image-label-scale, 1); content: 'gif'; position: absolute; line-height: 10px; diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js index 9f63feb6..8dc22823 100644 --- a/src/services/entity_normalizer/entity_normalizer.service.js +++ b/src/services/entity_normalizer/entity_normalizer.service.js @@ -54,6 +54,7 @@ export const parseUser = (data) => { return output } + output.emoji = data.emojis output.name = data.display_name output.name_html = addEmojis(escape(data.display_name), data.emojis) -- cgit v1.2.3-70-g09d2 From 8e9f5d7580d5a248e280b110218aa23052827789 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Mon, 7 Jun 2021 19:50:26 +0300 Subject: renamed StatusText to StatusBody for clarity, fixed chats --- src/components/chat_list_item/chat_list_item.js | 9 +- src/components/chat_list_item/chat_list_item.scss | 9 +- src/components/chat_list_item/chat_list_item.vue | 3 +- src/components/chat_message/chat_message.js | 2 + src/components/chat_message/chat_message.scss | 6 +- src/components/chat_message/chat_message.vue | 1 + .../settings_modal/tabs/theme_tab/theme_tab.js | 2 +- src/components/status/status.vue | 10 +- src/components/status_body/status_body.js | 147 +++++++++++++++++++++ src/components/status_body/status_body.scss | 112 ++++++++++++++++ src/components/status_body/status_body.vue | 95 +++++++++++++ src/components/status_content/status_content.js | 4 +- src/components/status_content/status_content.vue | 102 +------------- src/components/status_text/status_text.js | 147 --------------------- src/components/status_text/status_text.vue | 95 ------------- .../entity_normalizer/entity_normalizer.service.js | 2 + .../mini_html_converter.service.js | 2 +- yarn.lock | 81 ++++++++++-- 18 files changed, 460 insertions(+), 369 deletions(-) create mode 100644 src/components/status_body/status_body.js create mode 100644 src/components/status_body/status_body.scss create mode 100644 src/components/status_body/status_body.vue delete mode 100644 src/components/status_text/status_text.js delete mode 100644 src/components/status_text/status_text.vue (limited to 'src/services') diff --git a/src/components/chat_list_item/chat_list_item.js b/src/components/chat_list_item/chat_list_item.js index bee1ad53..e01b9bd4 100644 --- a/src/components/chat_list_item/chat_list_item.js +++ b/src/components/chat_list_item/chat_list_item.js @@ -1,5 +1,5 @@ import { mapState } from 'vuex' -import StatusContent from '../status_content/status_content.vue' +import StatusBody from '../status_content/status_content.vue' import fileType from 'src/services/file_type/file_type.service' import UserAvatar from '../user_avatar/user_avatar.vue' import AvatarList from '../avatar_list/avatar_list.vue' @@ -16,7 +16,7 @@ const ChatListItem = { AvatarList, Timeago, ChatTitle, - StatusContent + StatusBody }, computed: { ...mapState({ @@ -38,12 +38,15 @@ const ChatListItem = { }, messageForStatusContent () { const message = this.chat.lastMessage + const messageEmojis = message ? message.emojis : [] const isYou = message && message.account_id === this.currentUser.id - const content = message ? (this.attachmentInfo || message.content) : '' + const content = message ? (this.attachmentInfo || message.content_raw) : '' const messagePreview = isYou ? `${this.$t('chats.you')} ${content}` : content return { summary: '', + emojis: messageEmojis, statusnet_html: messagePreview, + raw_html: messagePreview, text: messagePreview, attachments: [] } diff --git a/src/components/chat_list_item/chat_list_item.scss b/src/components/chat_list_item/chat_list_item.scss index 9e97b28e..57332bed 100644 --- a/src/components/chat_list_item/chat_list_item.scss +++ b/src/components/chat_list_item/chat_list_item.scss @@ -77,18 +77,15 @@ border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius); } - .StatusContent { - img.emoji { - width: 1.4em; - height: 1.4em; - } + .chat-preview-body { + --emoji-size: 1.4em; } .time-wrapper { line-height: 1.4em; } - .single-line { + .chat-preview-body { padding-right: 1em; } } diff --git a/src/components/chat_list_item/chat_list_item.vue b/src/components/chat_list_item/chat_list_item.vue index cd3f436e..c7c0e878 100644 --- a/src/components/chat_list_item/chat_list_item.vue +++ b/src/components/chat_list_item/chat_list_item.vue @@ -29,7 +29,8 @@
- diff --git a/src/components/chat_message/chat_message.js b/src/components/chat_message/chat_message.js index bb380f87..d126d453 100644 --- a/src/components/chat_message/chat_message.js +++ b/src/components/chat_message/chat_message.js @@ -57,6 +57,8 @@ const ChatMessage = { messageForStatusContent () { return { summary: '', + emojis: this.message.emojis, + raw_html: this.message.content_raw, statusnet_html: this.message.content, text: this.message.content, attachments: this.message.attachments diff --git a/src/components/chat_message/chat_message.scss b/src/components/chat_message/chat_message.scss index e4351d3b..220355ea 100644 --- a/src/components/chat_message/chat_message.scss +++ b/src/components/chat_message/chat_message.scss @@ -89,8 +89,9 @@ } .without-attachment { - .status-content { - &::after { + .message-content { + // TODO figure out how to do it properly + .text::after { margin-right: 5.4em; content: " "; display: inline-block; @@ -162,6 +163,7 @@ .visible { opacity: 1; } + } .chat-message-date-separator { diff --git a/src/components/chat_message/chat_message.vue b/src/components/chat_message/chat_message.vue index 0f3fc97d..d62b831d 100644 --- a/src/components/chat_message/chat_message.vue +++ b/src/components/chat_message/chat_message.vue @@ -71,6 +71,7 @@
diff --git a/src/components/settings_modal/tabs/theme_tab/theme_tab.js b/src/components/settings_modal/tabs/theme_tab/theme_tab.js index 1388f74b..63416e93 100644 --- a/src/components/settings_modal/tabs/theme_tab/theme_tab.js +++ b/src/components/settings_modal/tabs/theme_tab/theme_tab.js @@ -474,7 +474,7 @@ export default { this.loadThemeFromLocalStorage(false, true) break case 'file': - console.err('Forcing snapshout from file is not supported yet') + console.error('Forcing snapshout from file is not supported yet') break } this.dismissWarning() diff --git a/src/components/status/status.vue b/src/components/status/status.vue index cc5fb79f..a4247cd4 100644 --- a/src/components/status/status.vue +++ b/src/components/status/status.vue @@ -89,7 +89,10 @@ v-if="retweeterHtml" :to="retweeterProfileLink" > - + - +

for paragraphs, GS uses
between them) + // as well as approximate line count by counting characters and approximating ~80 + // per line. + // + // Using max-height + overflow: auto for status components resulted in false positives + // very often with japanese characters, and it was very annoying. + tallStatus () { + const lengthScore = this.status.statusnet_html.split(/ 20 + }, + longSubject () { + return this.status.summary.length > 240 + }, + // When a status has a subject and is also tall, we should only have one show more/less button. If the default is to collapse statuses with subjects, we just treat it like a status with a subject; otherwise, we just treat it like a tall status. + mightHideBecauseSubject () { + return !!this.status.summary && this.localCollapseSubjectDefault + }, + mightHideBecauseTall () { + return this.tallStatus && !(this.status.summary && this.localCollapseSubjectDefault) + }, + hideSubjectStatus () { + return this.mightHideBecauseSubject && !this.expandingSubject + }, + hideTallStatus () { + return this.mightHideBecauseTall && !this.showingTall + }, + showingMore () { + return (this.mightHideBecauseTall && this.showingTall) || (this.mightHideBecauseSubject && this.expandingSubject) + }, + postBodyHtml () { + const html = this.status.raw_html + + if (this.mergedConfig.greentext) { + try { + if (html.includes('>')) { + // This checks if post has '>' at the beginning, excluding mentions so that @mention >impying works + return processHtml(html, (string) => { + if (string.includes('>') && + string + .replace(/<[^>]+?>/gi, '') // remove all tags + .replace(/@\w+/gi, '') // remove mentions (even failed ones) + .trim() + .startsWith('>')) { + return `${string}` + } else { + return string + } + }) + } else { + return html + } + } catch (e) { + console.error('Failed to process status html', e) + return html + } + } else { + return html + } + }, + attachmentTypes () { + return this.status.attachments.map(file => fileType.fileType(file.mimetype)) + }, + ...mapGetters(['mergedConfig']) + }, + components: { + RichContent + }, + mounted () { + this.status.attentions && this.status.attentions.forEach(attn => { + const { id } = attn + this.$store.dispatch('fetchUserIfMissing', id) + }) + }, + methods: { + linkClicked (event) { + const target = event.target.closest('.status-content a') + if (target) { + if (target.rel.match(/(?:^|\s)tag(?:$|\s)/) || target.className.match(/hashtag/)) { + // Extract tag name from dataset or link url + const tag = target.dataset.tag || extractTagFromUrl(target.href) + if (tag) { + const link = this.generateTagLink(tag) + this.$router.push(link) + return + } + } + window.open(target.href, '_blank') + } + }, + toggleShowMore () { + if (this.mightHideBecauseTall) { + this.showingTall = !this.showingTall + } else if (this.mightHideBecauseSubject) { + this.expandingSubject = !this.expandingSubject + } + }, + generateTagLink (tag) { + return `/tag/${tag}` + } + } +} + +export default StatusContent diff --git a/src/components/status_body/status_body.scss b/src/components/status_body/status_body.scss new file mode 100644 index 00000000..6282919c --- /dev/null +++ b/src/components/status_body/status_body.scss @@ -0,0 +1,112 @@ +@import '../../_variables.scss'; + +.StatusBody { + .emoji { + --_still_image-label-scale: 0.5; + } + + .summary { + display: block; + font-style: italic; + padding-bottom: 0.5em; + } + + .text { + &.-single-line { + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + height: 1.4em; + } + } + + .summary-wrapper { + margin-bottom: 0.5em; + border-style: solid; + border-width: 0 0 1px 0; + border-color: var(--border, $fallback--border); + flex-grow: 0; + + &.-tall { + position: relative; + + .summary { + max-height: 2em; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } + } + } + + .text-wrapper { + display: flex; + flex-direction: column; + flex-wrap: nowrap; + + &.-tall-status { + position: relative; + height: 220px; + overflow-x: hidden; + overflow-y: hidden; + z-index: 1; + + .text { + min-height: 0; + mask: + linear-gradient(to top, white, transparent) bottom/100% 70px no-repeat, + linear-gradient(to top, white, white); + + /* Autoprefixed seem to ignore this one, and also syntax is different */ + -webkit-mask-composite: xor; + mask-composite: exclude; + } + } + } + + & .tall-status-hider, + & .tall-subject-hider, + & .status-unhider, + & .cw-status-hider { + display: inline-block; + word-break: break-all; + width: 100%; + text-align: center; + } + + .tall-status-hider { + position: absolute; + height: 70px; + margin-top: 150px; + line-height: 110px; + z-index: 2; + } + + .tall-subject-hider { + // position: absolute; + padding-bottom: 0.5em; + } + + & .status-unhider, + & .cw-status-hider { + word-break: break-all; + + svg { + color: inherit; + } + } + + .greentext { + color: $fallback--cGreen; + color: var(--postGreentext, $fallback--cGreen); + } + + /* Not sure if this is necessary */ + video { + max-width: 100%; + max-height: 400px; + vertical-align: middle; + object-fit: contain; + } + +} diff --git a/src/components/status_body/status_body.vue b/src/components/status_body/status_body.vue new file mode 100644 index 00000000..6f982f2e --- /dev/null +++ b/src/components/status_body/status_body.vue @@ -0,0 +1,95 @@ + + + diff --git a/src/components/status_text/status_text.js b/src/components/status_text/status_text.js deleted file mode 100644 index e0b11edc..00000000 --- a/src/components/status_text/status_text.js +++ /dev/null @@ -1,147 +0,0 @@ -import fileType from 'src/services/file_type/file_type.service' -import RichContent from 'src/components/rich_content/rich_content.jsx' -import { processHtml } from 'src/services/tiny_post_html_processor/tiny_post_html_processor.service.js' -import { extractTagFromUrl } from 'src/services/matcher/matcher.service.js' -import { mapGetters } from 'vuex' -import { library } from '@fortawesome/fontawesome-svg-core' -import { - faFile, - faMusic, - faImage, - faLink, - faPollH -} from '@fortawesome/free-solid-svg-icons' - -library.add( - faFile, - faMusic, - faImage, - faLink, - faPollH -) - -const StatusContent = { - name: 'StatusContent', - props: [ - 'status', - 'focused', - 'noHeading', - 'fullContent', - 'singleLine' - ], - data () { - return { - showingTall: this.fullContent || (this.inConversation && this.focused), - showingLongSubject: false, - // not as computed because it sets the initial state which will be changed later - expandingSubject: !this.$store.getters.mergedConfig.collapseMessageWithSubject - } - }, - computed: { - localCollapseSubjectDefault () { - return this.mergedConfig.collapseMessageWithSubject - }, - // This is a bit hacky, but we want to approximate post height before rendering - // so we count newlines (masto uses

for paragraphs, GS uses
between them) - // as well as approximate line count by counting characters and approximating ~80 - // per line. - // - // Using max-height + overflow: auto for status components resulted in false positives - // very often with japanese characters, and it was very annoying. - tallStatus () { - const lengthScore = this.status.statusnet_html.split(/ 20 - }, - longSubject () { - return this.status.summary.length > 240 - }, - // When a status has a subject and is also tall, we should only have one show more/less button. If the default is to collapse statuses with subjects, we just treat it like a status with a subject; otherwise, we just treat it like a tall status. - mightHideBecauseSubject () { - return !!this.status.summary && this.localCollapseSubjectDefault - }, - mightHideBecauseTall () { - return this.tallStatus && !(this.status.summary && this.localCollapseSubjectDefault) - }, - hideSubjectStatus () { - return this.mightHideBecauseSubject && !this.expandingSubject - }, - hideTallStatus () { - return this.mightHideBecauseTall && !this.showingTall - }, - showingMore () { - return (this.mightHideBecauseTall && this.showingTall) || (this.mightHideBecauseSubject && this.expandingSubject) - }, - postBodyHtml () { - const html = this.status.raw_html - - if (this.mergedConfig.greentext) { - try { - if (html.includes('>')) { - // This checks if post has '>' at the beginning, excluding mentions so that @mention >impying works - return processHtml(html, (string) => { - if (string.includes('>') && - string - .replace(/<[^>]+?>/gi, '') // remove all tags - .replace(/@\w+/gi, '') // remove mentions (even failed ones) - .trim() - .startsWith('>')) { - return `${string}` - } else { - return string - } - }) - } else { - return html - } - } catch (e) { - console.err('Failed to process status html', e) - return html - } - } else { - return html - } - }, - attachmentTypes () { - return this.status.attachments.map(file => fileType.fileType(file.mimetype)) - }, - ...mapGetters(['mergedConfig']), - }, - components: { - RichContent - }, - mounted () { - this.status.attentions.forEach(attn => { - const { id } = attn - this.$store.dispatch('fetchUserIfMissing', id) - }) - }, - methods: { - linkClicked (event) { - const target = event.target.closest('.status-content a') - if (target) { - if (target.rel.match(/(?:^|\s)tag(?:$|\s)/) || target.className.match(/hashtag/)) { - // Extract tag name from dataset or link url - const tag = target.dataset.tag || extractTagFromUrl(target.href) - if (tag) { - const link = this.generateTagLink(tag) - this.$router.push(link) - return - } - } - window.open(target.href, '_blank') - } - }, - toggleShowMore () { - if (this.mightHideBecauseTall) { - this.showingTall = !this.showingTall - } else if (this.mightHideBecauseSubject) { - this.expandingSubject = !this.expandingSubject - } - }, - generateTagLink (tag) { - return `/tag/${tag}` - } - } -} - -export default StatusContent diff --git a/src/components/status_text/status_text.vue b/src/components/status_text/status_text.vue deleted file mode 100644 index bf723e15..00000000 --- a/src/components/status_text/status_text.vue +++ /dev/null @@ -1,95 +0,0 @@ - - diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js index 8dc22823..13162dcf 100644 --- a/src/services/entity_normalizer/entity_normalizer.service.js +++ b/src/services/entity_normalizer/entity_normalizer.service.js @@ -448,6 +448,8 @@ export const parseChatMessage = (message) => { output.id = message.id output.created_at = new Date(message.created_at) output.chat_id = message.chat_id + output.emojis = message.emojis + output.content_raw = message.content if (message.content) { output.content = addEmojis(message.content, message.emojis) } else { diff --git a/src/services/mini_html_converter/mini_html_converter.service.js b/src/services/mini_html_converter/mini_html_converter.service.js index 01f8adf8..900752cd 100644 --- a/src/services/mini_html_converter/mini_html_converter.service.js +++ b/src/services/mini_html_converter/mini_html_converter.service.js @@ -1,6 +1,6 @@ /** * This is a not-so-tiny purpose-built HTML parser/processor. It was made for use - * with StatusText component for purpose of replacing tags with vue components + * with StatusBody component for purpose of replacing tags with vue components * * known issue: doesn't handle CDATA so nested CDATA might not work well * diff --git a/yarn.lock b/yarn.lock index 23cc895b..9329cc3a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1011,23 +1011,86 @@ resolved "https://registry.yarnpkg.com/@ungap/event-target/-/event-target-0.1.0.tgz#88d527d40de86c4b0c99a060ca241d755999915b" integrity sha512-W2oyj0Fe1w/XhPZjkI3oUcDUAmu5P4qsdT2/2S8aMhtAWM/CE/jYWtji0pKNPDfxLI75fa5gWSEmnynKMNP/oA== -"@vue/babel-helper-vue-jsx-merge-props@^1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@vue/babel-helper-vue-jsx-merge-props/-/babel-helper-vue-jsx-merge-props-1.0.0.tgz#048fe579958da408fb7a8b2a3ec050b50a661040" - integrity sha512-6tyf5Cqm4m6v7buITuwS+jHzPlIPxbFzEhXR5JGZpbrvOcp1hiQKckd305/3C7C36wFekNTQSxAtgeM0j0yoUw== +"@vue/babel-helper-vue-jsx-merge-props@^1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@vue/babel-helper-vue-jsx-merge-props/-/babel-helper-vue-jsx-merge-props-1.2.1.tgz#31624a7a505fb14da1d58023725a4c5f270e6a81" + integrity sha512-QOi5OW45e2R20VygMSNhyQHvpdUwQZqGPc748JLGCYEy+yp8fNFNdbNIGAgZmi9e+2JHPd6i6idRuqivyicIkA== -"@vue/babel-plugin-transform-vue-jsx@^1.1.2": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@vue/babel-plugin-transform-vue-jsx/-/babel-plugin-transform-vue-jsx-1.1.2.tgz#c0a3e6efc022e75e4247b448a8fc6b86f03e91c0" - integrity sha512-YfdaoSMvD1nj7+DsrwfTvTnhDXI7bsuh+Y5qWwvQXlD24uLgnsoww3qbiZvWf/EoviZMrvqkqN4CBw0W3BWUTQ== +"@vue/babel-plugin-transform-vue-jsx@^1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@vue/babel-plugin-transform-vue-jsx/-/babel-plugin-transform-vue-jsx-1.2.1.tgz#646046c652c2f0242727f34519d917b064041ed7" + integrity sha512-HJuqwACYehQwh1fNT8f4kyzqlNMpBuUK4rSiSES5D4QsYncv5fxFsLyrxFPG2ksO7t5WP+Vgix6tt6yKClwPzA== dependencies: "@babel/helper-module-imports" "^7.0.0" "@babel/plugin-syntax-jsx" "^7.2.0" - "@vue/babel-helper-vue-jsx-merge-props" "^1.0.0" + "@vue/babel-helper-vue-jsx-merge-props" "^1.2.1" html-tags "^2.0.0" lodash.kebabcase "^4.1.1" svg-tags "^1.0.0" +"@vue/babel-preset-jsx@^1.2.4": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@vue/babel-preset-jsx/-/babel-preset-jsx-1.2.4.tgz#92fea79db6f13b01e80d3a0099e2924bdcbe4e87" + integrity sha512-oRVnmN2a77bYDJzeGSt92AuHXbkIxbf/XXSE3klINnh9AXBmVS1DGa1f0d+dDYpLfsAKElMnqKTQfKn7obcL4w== + dependencies: + "@vue/babel-helper-vue-jsx-merge-props" "^1.2.1" + "@vue/babel-plugin-transform-vue-jsx" "^1.2.1" + "@vue/babel-sugar-composition-api-inject-h" "^1.2.1" + "@vue/babel-sugar-composition-api-render-instance" "^1.2.4" + "@vue/babel-sugar-functional-vue" "^1.2.2" + "@vue/babel-sugar-inject-h" "^1.2.2" + "@vue/babel-sugar-v-model" "^1.2.3" + "@vue/babel-sugar-v-on" "^1.2.3" + +"@vue/babel-sugar-composition-api-inject-h@^1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@vue/babel-sugar-composition-api-inject-h/-/babel-sugar-composition-api-inject-h-1.2.1.tgz#05d6e0c432710e37582b2be9a6049b689b6f03eb" + integrity sha512-4B3L5Z2G+7s+9Bwbf+zPIifkFNcKth7fQwekVbnOA3cr3Pq71q71goWr97sk4/yyzH8phfe5ODVzEjX7HU7ItQ== + dependencies: + "@babel/plugin-syntax-jsx" "^7.2.0" + +"@vue/babel-sugar-composition-api-render-instance@^1.2.4": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@vue/babel-sugar-composition-api-render-instance/-/babel-sugar-composition-api-render-instance-1.2.4.tgz#e4cbc6997c344fac271785ad7a29325c51d68d19" + integrity sha512-joha4PZznQMsxQYXtR3MnTgCASC9u3zt9KfBxIeuI5g2gscpTsSKRDzWQt4aqNIpx6cv8On7/m6zmmovlNsG7Q== + dependencies: + "@babel/plugin-syntax-jsx" "^7.2.0" + +"@vue/babel-sugar-functional-vue@^1.2.2": + version "1.2.2" + resolved "https://registry.yarnpkg.com/@vue/babel-sugar-functional-vue/-/babel-sugar-functional-vue-1.2.2.tgz#267a9ac8d787c96edbf03ce3f392c49da9bd2658" + integrity sha512-JvbgGn1bjCLByIAU1VOoepHQ1vFsroSA/QkzdiSs657V79q6OwEWLCQtQnEXD/rLTA8rRit4rMOhFpbjRFm82w== + dependencies: + "@babel/plugin-syntax-jsx" "^7.2.0" + +"@vue/babel-sugar-inject-h@^1.2.2": + version "1.2.2" + resolved "https://registry.yarnpkg.com/@vue/babel-sugar-inject-h/-/babel-sugar-inject-h-1.2.2.tgz#d738d3c893367ec8491dcbb669b000919293e3aa" + integrity sha512-y8vTo00oRkzQTgufeotjCLPAvlhnpSkcHFEp60+LJUwygGcd5Chrpn5480AQp/thrxVm8m2ifAk0LyFel9oCnw== + dependencies: + "@babel/plugin-syntax-jsx" "^7.2.0" + +"@vue/babel-sugar-v-model@^1.2.3": + version "1.2.3" + resolved "https://registry.yarnpkg.com/@vue/babel-sugar-v-model/-/babel-sugar-v-model-1.2.3.tgz#fa1f29ba51ebf0aa1a6c35fa66d539bc459a18f2" + integrity sha512-A2jxx87mySr/ulAsSSyYE8un6SIH0NWHiLaCWpodPCVOlQVODCaSpiR4+IMsmBr73haG+oeCuSvMOM+ttWUqRQ== + dependencies: + "@babel/plugin-syntax-jsx" "^7.2.0" + "@vue/babel-helper-vue-jsx-merge-props" "^1.2.1" + "@vue/babel-plugin-transform-vue-jsx" "^1.2.1" + camelcase "^5.0.0" + html-tags "^2.0.0" + svg-tags "^1.0.0" + +"@vue/babel-sugar-v-on@^1.2.3": + version "1.2.3" + resolved "https://registry.yarnpkg.com/@vue/babel-sugar-v-on/-/babel-sugar-v-on-1.2.3.tgz#342367178586a69f392f04bfba32021d02913ada" + integrity sha512-kt12VJdz/37D3N3eglBywV8GStKNUhNrsxChXIV+o0MwVXORYuhDTHJRKPgLJRb/EY3vM2aRFQdxJBp9CLikjw== + dependencies: + "@babel/plugin-syntax-jsx" "^7.2.0" + "@vue/babel-plugin-transform-vue-jsx" "^1.2.1" + camelcase "^5.0.0" + "@vue/test-utils@^1.0.0-beta.26": version "1.0.0-beta.28" resolved "https://registry.yarnpkg.com/@vue/test-utils/-/test-utils-1.0.0-beta.28.tgz#767c43413df8cde86128735e58923803e444b9a5" -- cgit v1.2.3-70-g09d2 From 7d6fc044fbcf2c6f82f759605a0fd5c1aebae55a Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Mon, 7 Jun 2021 23:42:04 +0300 Subject: new mentions look --- src/components/mention_link/mention_link.js | 28 +++---- src/components/mention_link/mention_link.scss | 94 ++++++++++++++++++++--- src/components/mention_link/mention_link.vue | 12 +-- src/services/user_highlighter/user_highlighter.js | 14 +++- 4 files changed, 114 insertions(+), 34 deletions(-) (limited to 'src/services') diff --git a/src/components/mention_link/mention_link.js b/src/components/mention_link/mention_link.js index ade598d8..d26ae337 100644 --- a/src/components/mention_link/mention_link.js +++ b/src/components/mention_link/mention_link.js @@ -1,7 +1,6 @@ import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' -import { getTextColor, rgb2hex } from 'src/services/color_convert/color_convert.js' import { mapGetters, mapState } from 'vuex' -import { convert } from 'chromatism' +import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js' const MentionLink = { name: 'MentionLink', @@ -45,21 +44,22 @@ const MentionLink = { highlight () { return this.mergedConfig.highlight[this.user.screen_name] }, - bg () { - if (this.highlight) return this.highlight.color + highlightType () { + return this.highlight && ('-' + this.highlight.type) }, - text () { - if (this.bg) { - const linkColor = this.mergedConfig.customTheme.colors.link - const color = getTextColor(convert(this.bg).rgb, convert(linkColor).rgb) - return rgb2hex(color) - } + highlightClass () { + if (this.highlight) return highlightClass(this.user) }, style () { - return [ - this.bg && `--mention-bg: ${this.bg}`, - this.text && `--mention-text: ${this.text}` - ].filter(_ => _).join('; ') + if (this.highlight) { + const { + backgroundColor, + backgroundPosition, + backgroundImage, + ...rest + } = highlightStyle(this.highlight) + return rest + } }, ...mapGetters(['mergedConfig']), ...mapState({ diff --git a/src/components/mention_link/mention_link.scss b/src/components/mention_link/mention_link.scss index 237d9205..3a3e58d1 100644 --- a/src/components/mention_link/mention_link.scss +++ b/src/components/mention_link/mention_link.scss @@ -2,12 +2,10 @@ position: relative; white-space: normal; display: inline-block; + color: var(--link); & .new, - & .original, - & .full { - padding: 0 2px; - margin: 0 -2px; + & .original { display: inline-block; border-radius: 2px; } @@ -17,24 +15,95 @@ } .full { - pointer-events: none; position: absolute; + display: inline-block; + pointer-events: none; opacity: 0; - top: 0; - bottom: 0; + top: 100%; left: 0; + height: 100%; word-wrap: normal; white-space: nowrap; transition: opacity 0.2s ease; - background-color: var(--mention-bg, var(--popover)); - color: var(--mention-text, var(--link)); z-index: 1; + margin-top: 0.25em; + padding: 0.5em; } - .new { - background-color: var(--mention-bg); - color: var(--mention-text, var(--link)); + & .short, + & .full { + &::before { + content: '@'; + } + } + + & .new { + &, + &.-striped, + &.-solid, + &.-side { + .full { + } + .short { + &::before { + display: inline-block; + height: 50%; + font-size: 90%; + line-height: 1; + vertical-align: 6%; + } + } + + .you { + padding-right: 0.25em; + } + .short { + padding-left: 0.25em; + padding-right: 0; + padding-top: 0; + padding-bottom: 0; + } + + .userName { + color: var(--link); + margin-left: 0.25em; + padding-left: 0.25em; + padding-right: 0.25em; + padding-top: 0; + padding-bottom: 0; + } + } + + &.-striped { + & .userName, + & .full { + background-image: + repeating-linear-gradient( + 135deg, + var(--____highlight-tintColor), + var(--____highlight-tintColor) 5px, + var(--____highlight-tintColor2) 5px, + var(--____highlight-tintColor2) 10px + ); + } + } + &.-solid { + & .userName, + & .full { + background-image: linear-gradient(var(--____highlight-tintColor2), var(--____highlight-tintColor2)); + } + } + + &.-side { + & .userName, + & .userNameFull { + box-shadow: 0 -5px 3px -4px inset var(--____highlight-solidColor); + } + } + } + + .new { &.-you { & .shortName, & .full { @@ -45,5 +114,6 @@ &:hover .new .full { opacity: 1; + pointer-events: initial; } } diff --git a/src/components/mention_link/mention_link.vue b/src/components/mention_link/mention_link.vue index ea356315..5a8506eb 100644 --- a/src/components/mention_link/mention_link.vue +++ b/src/components/mention_link/mention_link.vue @@ -12,22 +12,24 @@ v-if="user" class="new" :style="style" - :class="{ '-you': isYou }" + :class="[{ '-you': isYou }, highlightType]" > - @ + diff --git a/src/services/user_highlighter/user_highlighter.js b/src/services/user_highlighter/user_highlighter.js index b91c0f78..3b07592e 100644 --- a/src/services/user_highlighter/user_highlighter.js +++ b/src/services/user_highlighter/user_highlighter.js @@ -8,6 +8,11 @@ const highlightStyle = (prefs) => { const solidColor = `rgb(${Math.floor(rgb.r)}, ${Math.floor(rgb.g)}, ${Math.floor(rgb.b)})` const tintColor = `rgba(${Math.floor(rgb.r)}, ${Math.floor(rgb.g)}, ${Math.floor(rgb.b)}, .1)` const tintColor2 = `rgba(${Math.floor(rgb.r)}, ${Math.floor(rgb.g)}, ${Math.floor(rgb.b)}, .2)` + const customProps = { + '--____highlight-solidColor': solidColor, + '--____highlight-tintColor': tintColor, + '--____highlight-tintColor2': tintColor2 + } if (type === 'striped') { return { backgroundImage: [ @@ -17,11 +22,13 @@ const highlightStyle = (prefs) => { `${tintColor2} 20px,`, `${tintColor2} 40px` ].join(' '), - backgroundPosition: '0 0' + backgroundPosition: '0 0', + ...customProps } } else if (type === 'solid') { return { - backgroundColor: tintColor2 + backgroundColor: tintColor2, + ...customProps } } else if (type === 'side') { return { @@ -31,7 +38,8 @@ const highlightStyle = (prefs) => { `${solidColor} 2px,`, `transparent 6px` ].join(' '), - backgroundPosition: '0 0' + backgroundPosition: '0 0', + ...customProps } } } -- cgit v1.2.3-70-g09d2 From cc00af7a3102034b05ebcd4aa1fd01c6f467184a Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Thu, 10 Jun 2021 18:52:01 +0300 Subject: Hellthread(tm) Certified --- src/components/mention_link/mention_link.js | 3 - src/components/mention_link/mention_link.vue | 1 - src/components/rich_content/rich_content.jsx | 176 ++++++++++++++++----- src/components/status/status.js | 3 + src/components/status/status.vue | 1 + src/components/status_body/status_body.js | 8 +- src/components/status_body/status_body.vue | 12 +- src/components/status_content/status_content.js | 1 + src/components/status_content/status_content.vue | 3 +- .../html_converter/html_line_converter.service.js | 102 ++++++++++++ .../html_converter/html_tree_converter.service.js | 146 +++++++++++++++++ .../mini_html_converter.service.js | 138 ---------------- .../tiny_post_html_processor.service.js | 94 ----------- .../html_converter/html_line_converter.spec.js | 130 +++++++++++++++ .../html_converter/html_tree_converter.spec.js | 166 +++++++++++++++++++ .../mini_post_html_processor.spec.js | 166 ------------------- .../tiny_post_html_processor.spec.js | 96 ----------- 17 files changed, 707 insertions(+), 539 deletions(-) create mode 100644 src/services/html_converter/html_line_converter.service.js create mode 100644 src/services/html_converter/html_tree_converter.service.js delete mode 100644 src/services/mini_html_converter/mini_html_converter.service.js delete mode 100644 src/services/tiny_post_html_processor/tiny_post_html_processor.service.js create mode 100644 test/unit/specs/services/html_converter/html_line_converter.spec.js create mode 100644 test/unit/specs/services/html_converter/html_tree_converter.spec.js delete mode 100644 test/unit/specs/services/tiny_post_html_processor/mini_post_html_processor.spec.js delete mode 100644 test/unit/specs/services/tiny_post_html_processor/tiny_post_html_processor.spec.js (limited to 'src/services') diff --git a/src/components/mention_link/mention_link.js b/src/components/mention_link/mention_link.js index 711c87d6..00b9e388 100644 --- a/src/components/mention_link/mention_link.js +++ b/src/components/mention_link/mention_link.js @@ -70,9 +70,6 @@ const MentionLink = { highlightClass () { if (this.highlight) return highlightClass(this.user) }, - oldPlace () { - return !this.mergedConfig.mentionsOwnLine - }, oldStyle () { return !this.mergedConfig.mentionsNewStyle }, diff --git a/src/components/mention_link/mention_link.vue b/src/components/mention_link/mention_link.vue index 281fab25..a65dbad3 100644 --- a/src/components/mention_link/mention_link.vue +++ b/src/components/mention_link/mention_link.vue @@ -1,7 +1,6 @@ @@ -281,9 +264,10 @@ .user-card { position: relative; - &:hover .Avatar { + &:hover { --_still-image-img-visibility: visible; --_still-image-canvas-visibility: hidden; + --_still-image-label-visibility: hidden; } .panel-heading { @@ -327,12 +311,12 @@ } } - p { - margin-bottom: 0; - } - &-bio { text-align: center; + display: block; + line-height: 18px; + padding: 1em; + margin: 0; a { color: $fallback--link; @@ -344,11 +328,6 @@ vertical-align: middle; max-width: 100%; max-height: 400px; - - &.emoji { - width: 32px; - height: 32px; - } } } @@ -450,13 +429,6 @@ // big one z-index: 1; - img { - width: 26px; - height: 26px; - vertical-align: middle; - object-fit: contain - } - .top-line { display: flex; } @@ -469,12 +441,7 @@ margin-right: 1em; font-size: 15px; - img { - object-fit: contain; - height: 16px; - width: 16px; - vertical-align: middle; - } + --emoji-size: 14px; } .bottom-line { diff --git a/src/components/user_profile/user_profile.js b/src/components/user_profile/user_profile.js index c0b55a6c..7a475609 100644 --- a/src/components/user_profile/user_profile.js +++ b/src/components/user_profile/user_profile.js @@ -4,6 +4,7 @@ import FollowCard from '../follow_card/follow_card.vue' import Timeline from '../timeline/timeline.vue' import Conversation from '../conversation/conversation.vue' import TabSwitcher from 'src/components/tab_switcher/tab_switcher.js' +import RichContent from 'src/components/rich_content/rich_content.jsx' import List from '../list/list.vue' import withLoadMore from '../../hocs/with_load_more/with_load_more' import { library } from '@fortawesome/fontawesome-svg-core' @@ -164,7 +165,8 @@ const UserProfile = { FriendList, FollowCard, TabSwitcher, - Conversation + Conversation, + RichContent } } diff --git a/src/components/user_profile/user_profile.vue b/src/components/user_profile/user_profile.vue index aef897ae..726216ff 100644 --- a/src/components/user_profile/user_profile.vue +++ b/src/components/user_profile/user_profile.vue @@ -20,20 +20,24 @@ :key="index" class="user-profile-field" > -

{ output.emoji = data.emojis output.name = data.display_name - output.name_html = addEmojis(escape(data.display_name), data.emojis) + output.name_html = escape(data.display_name) output.description = data.note - output.description_html = addEmojis(data.note, data.emojis) + // TODO cleanup this shit, output.description is overriden with source data + output.description_html = data.note output.fields = data.fields output.fields_html = data.fields.map(field => { -- cgit v1.2.3-70-g09d2 From 97e86381c868fbd49a33e190722934dee698600d Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Fri, 13 Aug 2021 13:12:33 +0300 Subject: remove old emoji added, everything emoji-bearing uses RichContent now --- .../entity_normalizer/entity_normalizer.service.js | 14 ++------ .../entity_normalizer/entity_normalizer.spec.js | 37 +--------------------- 2 files changed, 3 insertions(+), 48 deletions(-) (limited to 'src/services') diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js index d84e3422..04bb45a4 100644 --- a/src/services/entity_normalizer/entity_normalizer.service.js +++ b/src/services/entity_normalizer/entity_normalizer.service.js @@ -65,8 +65,8 @@ export const parseUser = (data) => { output.fields = data.fields output.fields_html = data.fields.map(field => { return { - name: addEmojis(escape(field.name), data.emojis), - value: addEmojis(field.value, data.emojis) + name: escape(field.name), + value: field.value } }) output.fields_text = data.fields.map(field => { @@ -241,16 +241,6 @@ export const parseAttachment = (data) => { return output } -export const addEmojis = (string, emojis) => { - const matchOperatorsRegex = /[|\\{}()[\]^$+*?.-]/g - return emojis.reduce((acc, emoji) => { - const regexSafeShortCode = emoji.shortcode.replace(matchOperatorsRegex, '\\$&') - return acc.replace( - new RegExp(`:${regexSafeShortCode}:`, 'g'), - `:${emoji.shortcode}:` - ) - }, string) -} export const parseStatus = (data) => { const output = {} diff --git a/test/unit/specs/services/entity_normalizer/entity_normalizer.spec.js b/test/unit/specs/services/entity_normalizer/entity_normalizer.spec.js index 8a5a6ef9..8932bc7c 100644 --- a/test/unit/specs/services/entity_normalizer/entity_normalizer.spec.js +++ b/test/unit/specs/services/entity_normalizer/entity_normalizer.spec.js @@ -1,4 +1,4 @@ -import { parseStatus, parseUser, parseNotification, addEmojis, parseLinkHeaderPagination } from '../../../../../src/services/entity_normalizer/entity_normalizer.service.js' +import { parseStatus, parseUser, parseNotification, parseLinkHeaderPagination } from '../../../../../src/services/entity_normalizer/entity_normalizer.service.js' import mastoapidata from '../../../../fixtures/mastoapi.json' import qvitterapidata from '../../../../fixtures/statuses.json' @@ -338,41 +338,6 @@ describe('API Entities normalizer', () => { }) }) - describe('MastoAPI emoji adder', () => { - const emojis = makeMockEmojiMasto() - const imageHtml = ':image:' - .replace(/"/g, '\'') - const thinkHtml = ':thinking:' - .replace(/"/g, '\'') - - it('correctly replaces shortcodes in supplied string', () => { - const result = addEmojis('This post has :image: emoji and :thinking: emoji', emojis) - expect(result).to.include(thinkHtml) - expect(result).to.include(imageHtml) - }) - - it('handles consecutive emojis correctly', () => { - const result = addEmojis('Lelel emoji spam :thinking::thinking::thinking::thinking:', emojis) - expect(result).to.include(thinkHtml + thinkHtml + thinkHtml + thinkHtml) - }) - - it('Doesn\'t replace nonexistent emojis', () => { - const result = addEmojis('Admin add the :tenshi: emoji', emojis) - expect(result).to.equal('Admin add the :tenshi: emoji') - }) - - it('Doesn\'t blow up on regex special characters', () => { - const emojis = makeMockEmojiMasto([{ - shortcode: 'c++' - }, { - shortcode: '[a-z] {|}*' - }]) - const result = addEmojis('This post has :c++: emoji and :[a-z] {|}*: emoji', emojis) - expect(result).to.include('title=\':c++:\'') - expect(result).to.include('title=\':[a-z] {|}*:\'') - }) - }) - describe('Link header pagination', () => { it('Parses min and max ids as integers', () => { const linkHeader = '; rel="next", ; rel="prev"' -- cgit v1.2.3-70-g09d2 From f16658adfc897a3b07ed7f79d872acd2c3837cc8 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Sun, 15 Aug 2021 02:59:14 +0300 Subject: fix tests --- src/services/html_converter/html_line_converter.service.js | 2 +- src/services/html_converter/html_tree_converter.service.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) (limited to 'src/services') diff --git a/src/services/html_converter/html_line_converter.service.js b/src/services/html_converter/html_line_converter.service.js index 74103b02..5eeaa7cb 100644 --- a/src/services/html_converter/html_line_converter.service.js +++ b/src/services/html_converter/html_line_converter.service.js @@ -18,7 +18,7 @@ import { getTagName } from './utility.service.js' * @param {Object} input - input data * @return {(string|{ text: string })[]} processed html in form of a list. */ -export const convertHtmlToLines = (html) => { +export const convertHtmlToLines = (html = '') => { // Elements that are implicitly self-closing // https://developer.mozilla.org/en-US/docs/Glossary/empty_element const emptyElements = new Set([ diff --git a/src/services/html_converter/html_tree_converter.service.js b/src/services/html_converter/html_tree_converter.service.js index 804d35d7..6a8796c4 100644 --- a/src/services/html_converter/html_tree_converter.service.js +++ b/src/services/html_converter/html_tree_converter.service.js @@ -19,7 +19,7 @@ import { getTagName } from './utility.service.js' * @param {Object} input - input data * @return {string} processed html */ -export const convertHtmlToTree = (html) => { +export const convertHtmlToTree = (html = '') => { // Elements that are implicitly self-closing // https://developer.mozilla.org/en-US/docs/Glossary/empty_element const emptyElements = new Set([ -- cgit v1.2.3-70-g09d2 From d36b45ad436fbbb027bfe6af1093107c0bfed61f Mon Sep 17 00:00:00 2001 From: rinpatch Date: Tue, 16 Nov 2021 19:49:01 +0300 Subject: entity_normalizer: Escape name when parsing user In January 2020 Pleroma backend stopped escaping HTML in display names and passed that responsibility on frontends, compliant with Mastodon's version of Mastodon API [1]. Pleroma-FE was subsequently modified to escape the display name [2], however only in the "name_html" field. This was fine however, since that's what the code rendering display names used. However, 2 months ago an MR [3] refactoring the way the frontend does emoji and mention rendering was merged. One of the things it did was moving away from doing emoji rendering in the entity normalizer and use the unescaped 'user.name' in the rendering code, resulting in HTML injection being possible again. This patch escapes 'user.name' as well, as far as I can tell there is no actual use for an unescaped display name in frontend code, especially when it comes from MastoAPI, where it is not supposed to be HTML. [1]: https://git.pleroma.social/pleroma/pleroma-fe/-/merge_requests/1052 [2]: https://git.pleroma.social/pleroma/pleroma/-/merge_requests/2167 [3]: https://git.pleroma.social/pleroma/pleroma-fe/-/merge_requests/1392 --- src/components/settings_modal/tabs/profile_tab.js | 2 +- src/services/entity_normalizer/entity_normalizer.service.js | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) (limited to 'src/services') diff --git a/src/components/settings_modal/tabs/profile_tab.js b/src/components/settings_modal/tabs/profile_tab.js index 9709424c..64079fcd 100644 --- a/src/components/settings_modal/tabs/profile_tab.js +++ b/src/components/settings_modal/tabs/profile_tab.js @@ -24,7 +24,7 @@ library.add( const ProfileTab = { data () { return { - newName: this.$store.state.users.currentUser.name, + newName: this.$store.state.users.currentUser.name_unescaped, newBio: unescape(this.$store.state.users.currentUser.description), newLocked: this.$store.state.users.currentUser.locked, newNoRichText: this.$store.state.users.currentUser.no_rich_text, diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js index 04bb45a4..7025d803 100644 --- a/src/services/entity_normalizer/entity_normalizer.service.js +++ b/src/services/entity_normalizer/entity_normalizer.service.js @@ -55,8 +55,9 @@ export const parseUser = (data) => { } output.emoji = data.emojis - output.name = data.display_name - output.name_html = escape(data.display_name) + output.name = escape(data.display_name) + output.name_html = output.name + output.name_unescaped = data.display_name output.description = data.note // TODO cleanup this shit, output.description is overriden with source data -- cgit v1.2.3-70-g09d2 From 571e73a346996f1bcdecb6545b44351a65fee3cb Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Thu, 3 Feb 2022 23:13:28 +0200 Subject: better approach to unescaping --- src/components/rich_content/rich_content.jsx | 2 +- src/services/html_converter/html_tree_converter.service.js | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) (limited to 'src/services') diff --git a/src/components/rich_content/rich_content.jsx b/src/components/rich_content/rich_content.jsx index 8ebabad7..46bc661a 100644 --- a/src/components/rich_content/rich_content.jsx +++ b/src/components/rich_content/rich_content.jsx @@ -60,7 +60,7 @@ export default Vue.component('RichContent', { // NEVER EVER TOUCH DATA INSIDE RENDER render (h) { // Pre-process HTML - const { newHtml: html } = preProcessPerLine(unescape(this.html), this.greentext) + const { newHtml: html } = preProcessPerLine(this.html, this.greentext) let currentMentions = null // Current chain of mentions, we group all mentions together // This is used to recover spacing removed when parsing mentions let lastSpacing = '' diff --git a/src/services/html_converter/html_tree_converter.service.js b/src/services/html_converter/html_tree_converter.service.js index 6a8796c4..247a8173 100644 --- a/src/services/html_converter/html_tree_converter.service.js +++ b/src/services/html_converter/html_tree_converter.service.js @@ -1,4 +1,5 @@ import { getTagName } from './utility.service.js' +import { unescape } from 'lodash' /** * This is a not-so-tiny purpose-built HTML parser/processor. This parses html @@ -49,7 +50,7 @@ export const convertHtmlToTree = (html = '') => { const handleOpen = (tag) => { const curBuf = getCurrentBuffer() - const newLevel = [tag, []] + const newLevel = [unescape(tag), []] levels.push(newLevel) curBuf.push(newLevel) } -- cgit v1.2.3-70-g09d2 From 9c1814d12243f45cb67a797780a8c393f301080c Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Tue, 22 Feb 2022 23:31:40 +0200 Subject: expert settings toggle + server-side settings --- .../settings_modal/helpers/boolean_setting.js | 15 ++- .../settings_modal/helpers/boolean_setting.vue | 2 + .../settings_modal/helpers/choice_setting.js | 8 +- .../settings_modal/helpers/choice_setting.vue | 2 + .../helpers/server_side_indicator.vue | 51 +++++++++ .../helpers/shared_computed_object.js | 9 ++ src/components/settings_modal/settings_modal.js | 11 ++ src/components/settings_modal/settings_modal.scss | 7 ++ src/components/settings_modal/settings_modal.vue | 6 +- .../settings_modal/tabs/filtering_tab.vue | 46 +------- src/components/settings_modal/tabs/general_tab.js | 11 +- src/components/settings_modal/tabs/general_tab.vue | 122 +++++++++++--------- .../settings_modal/tabs/notifications_tab.js | 8 +- .../settings_modal/tabs/notifications_tab.vue | 72 +++++++++--- src/components/settings_modal/tabs/profile_tab.js | 23 +--- src/components/settings_modal/tabs/profile_tab.vue | 124 ++++++++++----------- src/i18n/en.json | 5 + src/main.js | 2 + src/modules/config.js | 1 + src/modules/serverSideConfig.js | 111 ++++++++++++++++++ .../entity_normalizer/entity_normalizer.service.js | 1 + 21 files changed, 433 insertions(+), 204 deletions(-) create mode 100644 src/components/settings_modal/helpers/server_side_indicator.vue create mode 100644 src/modules/serverSideConfig.js (limited to 'src/services') diff --git a/src/components/settings_modal/helpers/boolean_setting.js b/src/components/settings_modal/helpers/boolean_setting.js index 5c52f697..353e551c 100644 --- a/src/components/settings_modal/helpers/boolean_setting.js +++ b/src/components/settings_modal/helpers/boolean_setting.js @@ -1,14 +1,17 @@ import { get, set } from 'lodash' import Checkbox from 'src/components/checkbox/checkbox.vue' import ModifiedIndicator from './modified_indicator.vue' +import ServerSideIndicator from './server_side_indicator.vue' export default { components: { Checkbox, - ModifiedIndicator + ModifiedIndicator, + ServerSideIndicator }, props: [ 'path', - 'disabled' + 'disabled', + 'expert' ], computed: { pathDefault () { @@ -26,8 +29,14 @@ export default { defaultState () { return get(this.$parent, this.pathDefault) }, + isServerSide () { + return this.path.startsWith('serverSide_') + }, isChanged () { - return this.state !== this.defaultState + return !this.path.startsWith('serverSide_') && this.state !== this.defaultState + }, + matchesExpertLevel () { + return (this.expert || 0) <= this.$parent.expertLevel } }, methods: { diff --git a/src/components/settings_modal/helpers/boolean_setting.vue b/src/components/settings_modal/helpers/boolean_setting.vue index c3ee6583..c82cf23f 100644 --- a/src/components/settings_modal/helpers/boolean_setting.vue +++ b/src/components/settings_modal/helpers/boolean_setting.vue @@ -1,6 +1,7 @@ diff --git a/src/components/settings_modal/helpers/choice_setting.js b/src/components/settings_modal/helpers/choice_setting.js index a15f6bac..07d0f76d 100644 --- a/src/components/settings_modal/helpers/choice_setting.js +++ b/src/components/settings_modal/helpers/choice_setting.js @@ -9,7 +9,8 @@ export default { props: [ 'path', 'disabled', - 'options' + 'options', + 'expert' ], computed: { pathDefault () { @@ -28,7 +29,10 @@ export default { return get(this.$parent, this.pathDefault) }, isChanged () { - return this.state !== this.defaultState + return !this.path.startsWith('serverSide_') && this.state !== this.defaultState + }, + matchesExpertLevel () { + return (this.expert || 0) <= this.$parent.expertLevel } }, methods: { diff --git a/src/components/settings_modal/helpers/choice_setting.vue b/src/components/settings_modal/helpers/choice_setting.vue index fa17661b..845886ca 100644 --- a/src/components/settings_modal/helpers/choice_setting.vue +++ b/src/components/settings_modal/helpers/choice_setting.vue @@ -1,6 +1,7 @@ diff --git a/src/components/settings_modal/helpers/server_side_indicator.vue b/src/components/settings_modal/helpers/server_side_indicator.vue new file mode 100644 index 00000000..143a86a1 --- /dev/null +++ b/src/components/settings_modal/helpers/server_side_indicator.vue @@ -0,0 +1,51 @@ + + + + + diff --git a/src/components/settings_modal/helpers/shared_computed_object.js b/src/components/settings_modal/helpers/shared_computed_object.js index 2c833c0c..12431dca 100644 --- a/src/components/settings_modal/helpers/shared_computed_object.js +++ b/src/components/settings_modal/helpers/shared_computed_object.js @@ -1,4 +1,5 @@ import { defaultState as configDefaultState } from 'src/modules/config.js' +import { defaultState as serverSideConfigDefaultState } from 'src/modules/serverSideConfig.js' const SharedComputedObject = () => ({ user () { @@ -22,6 +23,14 @@ const SharedComputedObject = () => ({ } }]) .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}), + ...Object.keys(serverSideConfigDefaultState) + .map(key => ['serverSide_' + key, { + get () { return this.$store.state.serverSideConfig[key] }, + set (value) { + this.$store.dispatch('setServerSideOption', { name: key, value }) + } + }]) + .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}), // Special cases (need to transform values or perform actions first) useStreamingApi: { get () { return this.$store.getters.mergedConfig.useStreamingApi }, diff --git a/src/components/settings_modal/settings_modal.js b/src/components/settings_modal/settings_modal.js index 04043483..82ea410e 100644 --- a/src/components/settings_modal/settings_modal.js +++ b/src/components/settings_modal/settings_modal.js @@ -3,6 +3,7 @@ import PanelLoading from 'src/components/panel_loading/panel_loading.vue' import AsyncComponentError from 'src/components/async_component_error/async_component_error.vue' import getResettableAsyncComponent from 'src/services/resettable_async_component.js' import Popover from '../popover/popover.vue' +import Checkbox from 'src/components/checkbox/checkbox.vue' import { library } from '@fortawesome/fontawesome-svg-core' import { cloneDeep } from 'lodash' import { @@ -51,6 +52,7 @@ const SettingsModal = { components: { Modal, Popover, + Checkbox, SettingsModalContent: getResettableAsyncComponent( () => import('./settings_modal_content.vue'), { @@ -159,6 +161,15 @@ const SettingsModal = { }, modalPeeked () { return this.$store.state.interface.settingsModalState === 'minimized' + }, + expertLevel: { + get () { + return this.$store.state.config.expertLevel > 0 + }, + set (value) { + console.log(value) + this.$store.dispatch('setOption', { name: 'expertLevel', value: value ? 1 : 0 }) + } } } } diff --git a/src/components/settings_modal/settings_modal.scss b/src/components/settings_modal/settings_modal.scss index 90446b36..fb466f2f 100644 --- a/src/components/settings_modal/settings_modal.scss +++ b/src/components/settings_modal/settings_modal.scss @@ -48,4 +48,11 @@ } } } + + .settings-footer { + display: flex; + >* { + margin-right: 0.5em; + } + } } diff --git a/src/components/settings_modal/settings_modal.vue b/src/components/settings_modal/settings_modal.vue index 583c2ecc..0aad1abb 100644 --- a/src/components/settings_modal/settings_modal.vue +++ b/src/components/settings_modal/settings_modal.vue @@ -53,7 +53,7 @@
- diff --git a/src/components/settings_modal/tabs/filtering_tab.vue b/src/components/settings_modal/tabs/filtering_tab.vue index 50ee20e0..cd7f0bc4 100644 --- a/src/components/settings_modal/tabs/filtering_tab.vue +++ b/src/components/settings_modal/tabs/filtering_tab.vue @@ -38,7 +38,7 @@
  • - + {{ $t('settings.hide_post_stats') }}
  • @@ -59,7 +59,7 @@
    {{ $t('settings.filtering_explanation') }}

    {{ $t('settings.attachments') }}

    -
  • +
  • @@ -84,7 +84,7 @@
  • -
    +

    {{ $t('settings.user_profiles') }}

    • @@ -94,46 +94,6 @@
    -
    -

    {{ $t('settings.notifications') }}

    -
      -
    • - {{ $t('settings.notification_visibility') }} -
        -
      • - - {{ $t('settings.notification_visibility_likes') }} - -
      • -
      • - - {{ $t('settings.notification_visibility_repeats') }} - -
      • -
      • - - {{ $t('settings.notification_visibility_follows') }} - -
      • -
      • - - {{ $t('settings.notification_visibility_mentions') }} - -
      • -
      • - - {{ $t('settings.notification_visibility_moves') }} - -
      • -
      • - - {{ $t('settings.notification_visibility_emoji_reactions') }} - -
      • -
      -
    • -
    -
    diff --git a/src/components/settings_modal/tabs/general_tab.js b/src/components/settings_modal/tabs/general_tab.js index 952c328d..9e4e282f 100644 --- a/src/components/settings_modal/tabs/general_tab.js +++ b/src/components/settings_modal/tabs/general_tab.js @@ -1,8 +1,10 @@ import BooleanSetting from '../helpers/boolean_setting.vue' import ChoiceSetting from '../helpers/choice_setting.vue' +import ScopeSelector from 'src/components/scope_selector/scope_selector.vue' import InterfaceLanguageSwitcher from 'src/components/interface_language_switcher/interface_language_switcher.vue' import SharedComputedObject from '../helpers/shared_computed_object.js' +import ServerSideIndicator from '../helpers/server_side_indicator.vue' import { library } from '@fortawesome/fontawesome-svg-core' import { faGlobe @@ -37,7 +39,9 @@ const GeneralTab = { components: { BooleanSetting, ChoiceSetting, - InterfaceLanguageSwitcher + InterfaceLanguageSwitcher, + ScopeSelector, + ServerSideIndicator, }, computed: { postFormats () { @@ -57,6 +61,11 @@ const GeneralTab = { }, instanceShoutboxPresent () { return this.$store.state.instance.shoutAvailable }, ...SharedComputedObject() + }, + methods: { + changeDefaultScope (value) { + this.$store.dispatch('setServerSideOption', { name: 'defaultScope', value }) + } } } diff --git a/src/components/settings_modal/tabs/general_tab.vue b/src/components/settings_modal/tabs/general_tab.vue index eba3b268..4accf0c1 100644 --- a/src/components/settings_modal/tabs/general_tab.vue +++ b/src/components/settings_modal/tabs/general_tab.vue @@ -27,7 +27,7 @@
  • - + {{ $t('settings.streaming') }}
      {{ $t('settings.pause_on_unfocused') }} @@ -45,7 +46,7 @@
  • - + {{ $t('settings.useStreamingApi') }}
    @@ -54,17 +55,22 @@
  • - + {{ $t('settings.virtual_scrolling') }}
  • - + + {{ $t('settings.always_show_post_button') }} + +
  • +
  • + {{ $t('settings.autohide_floating_post_button') }}
  • - + {{ $t('settings.hide_shoutbox') }}
  • @@ -79,13 +85,18 @@
  • - + {{ $t('settings.emoji_reactions_on_timeline') }}
  • +
  • + + {{ $t('settings.no_rich_text_description') }} + +
  • {{ $t('settings.attachments') }}

  • - + {{ $t('settings.use_contain_fit') }}
  • @@ -97,7 +108,7 @@
    • {{ $t('settings.preload_images') }} @@ -105,7 +116,7 @@
    • {{ $t('settings.use_one_click_nsfw') }} @@ -113,7 +124,7 @@
  • - + {{ $t('settings.loop_video') }}
    • {{ $t('settings.loop_video_silent_only') }} @@ -137,21 +148,11 @@
  • - + {{ $t('settings.play_videos_in_modal') }}
  • -

    {{ $t('settings.fun') }}

    -
  • - - {{ $t('settings.greentext') }} - -
  • -
  • - - {{ $t('settings.show_yous') }} - -
  • +

    {{ $t('settings.mention_links') }}

  • -
  • - +
  • + {{ $t('settings.mention_link_show_tooltip') }}
  • +
  • - + {{ $t('settings.use_at_icon') }}
  • @@ -182,29 +182,56 @@
  • - + {{ $t('settings.mention_link_fade_domain') }}
  • -
  • - +
  • + {{ $t('settings.mention_link_bolden_you') }}
  • - +

    {{ $t('settings.fun') }}

    +
  • + + {{ $t('settings.greentext') }} + +
  • +
  • + + {{ $t('settings.show_yous') }} + +
  • -
    +

    {{ $t('settings.composing') }}

    • - + +
    • +
    • + + {{ $t('settings.sensitive_by_default') }} + +
    • +
    • + {{ $t('settings.scope_copy') }}
    • - + {{ $t('settings.subject_input_always_show') }}
    • @@ -213,6 +240,7 @@ id="subjectLineBehavior" path="subjectLineBehavior" :options="subjectLineOptions" + expert="1" > {{ $t('settings.subject_line_behavior') }} @@ -227,43 +255,27 @@
    • - + {{ $t('settings.minimal_scopes_mode') }}
    • - - {{ $t('settings.sensitive_by_default') }} - -
    • -
    • - + {{ $t('settings.always_show_post_button') }}
    • - + {{ $t('settings.autohide_floating_post_button') }}
    • - + {{ $t('settings.pad_emoji') }}
    - -
    -

    {{ $t('settings.notifications') }}

    -
      -
    • - - {{ $t('settings.enable_web_push_notifications') }} - -
    • -
    -
    diff --git a/src/components/settings_modal/tabs/notifications_tab.js b/src/components/settings_modal/tabs/notifications_tab.js index 3e44c95d..3c6ab87f 100644 --- a/src/components/settings_modal/tabs/notifications_tab.js +++ b/src/components/settings_modal/tabs/notifications_tab.js @@ -1,4 +1,5 @@ -import Checkbox from 'src/components/checkbox/checkbox.vue' +import BooleanSetting from '../helpers/boolean_setting.vue' +import SharedComputedObject from '../helpers/shared_computed_object.js' const NotificationsTab = { data () { @@ -9,12 +10,13 @@ const NotificationsTab = { } }, components: { - Checkbox + BooleanSetting }, computed: { user () { return this.$store.state.users.currentUser - } + }, + ...SharedComputedObject() }, methods: { updateNotificationSettings () { diff --git a/src/components/settings_modal/tabs/notifications_tab.vue b/src/components/settings_modal/tabs/notifications_tab.vue index 7e0568ea..5e9ce91e 100644 --- a/src/components/settings_modal/tabs/notifications_tab.vue +++ b/src/components/settings_modal/tabs/notifications_tab.vue @@ -2,30 +2,68 @@

    {{ $t('settings.notification_setting_filters') }}

    -

    - - {{ $t('settings.notification_setting_block_from_strangers') }} - -

    +
      +
    • + + {{ $t('settings.notification_setting_block_from_strangers') }} + +
    • +
    • + {{ $t('settings.notification_visibility') }} +
        +
      • + + {{ $t('settings.notification_visibility_likes') }} + +
      • +
      • + + {{ $t('settings.notification_visibility_repeats') }} + +
      • +
      • + + {{ $t('settings.notification_visibility_follows') }} + +
      • +
      • + + {{ $t('settings.notification_visibility_mentions') }} + +
      • +
      • + + {{ $t('settings.notification_visibility_moves') }} + +
      • +
      • + + {{ $t('settings.notification_visibility_emoji_reactions') }} + +
      • +
      +
    • +
    -
    +

    {{ $t('settings.notification_setting_privacy') }}

    -

    - - {{ $t('settings.notification_setting_hide_notification_contents') }} - -

    +
      +
    • + + {{ $t('settings.enable_web_push_notifications') }} + +
    • +
    • + + {{ $t('settings.notification_setting_hide_notification_contents') }} + +
    • +

    {{ $t('settings.notification_mutes') }}

    {{ $t('settings.notification_blocks') }}

    -
    diff --git a/src/components/settings_modal/tabs/profile_tab.js b/src/components/settings_modal/tabs/profile_tab.js index 64079fcd..bee8a7bb 100644 --- a/src/components/settings_modal/tabs/profile_tab.js +++ b/src/components/settings_modal/tabs/profile_tab.js @@ -8,6 +8,9 @@ import EmojiInput from 'src/components/emoji_input/emoji_input.vue' import suggestor from 'src/components/emoji_input/suggestor.js' import Autosuggest from 'src/components/autosuggest/autosuggest.vue' import Checkbox from 'src/components/checkbox/checkbox.vue' +import BooleanSetting from '../helpers/boolean_setting.vue' +import SharedComputedObject from '../helpers/shared_computed_object.js' + import { library } from '@fortawesome/fontawesome-svg-core' import { faTimes, @@ -27,18 +30,10 @@ const ProfileTab = { newName: this.$store.state.users.currentUser.name_unescaped, newBio: unescape(this.$store.state.users.currentUser.description), newLocked: this.$store.state.users.currentUser.locked, - newNoRichText: this.$store.state.users.currentUser.no_rich_text, - newDefaultScope: this.$store.state.users.currentUser.default_scope, newFields: this.$store.state.users.currentUser.fields.map(field => ({ name: field.name, value: field.value })), - hideFollows: this.$store.state.users.currentUser.hide_follows, - hideFollowers: this.$store.state.users.currentUser.hide_followers, - hideFollowsCount: this.$store.state.users.currentUser.hide_follows_count, - hideFollowersCount: this.$store.state.users.currentUser.hide_followers_count, showRole: this.$store.state.users.currentUser.show_role, role: this.$store.state.users.currentUser.role, - discoverable: this.$store.state.users.currentUser.discoverable, bot: this.$store.state.users.currentUser.bot, - allowFollowingMove: this.$store.state.users.currentUser.allow_following_move, pickAvatarBtnVisible: true, bannerUploading: false, backgroundUploading: false, @@ -54,12 +49,14 @@ const ProfileTab = { EmojiInput, Autosuggest, ProgressButton, - Checkbox + Checkbox, + BooleanSetting }, computed: { user () { return this.$store.state.users.currentUser }, + ...SharedComputedObject(), emojiUserSuggestor () { return suggestor({ emoji: [ @@ -123,15 +120,7 @@ const ProfileTab = { /* eslint-disable camelcase */ display_name: this.newName, fields_attributes: this.newFields.filter(el => el != null), - default_scope: this.newDefaultScope, - no_rich_text: this.newNoRichText, - hide_follows: this.hideFollows, - hide_followers: this.hideFollowers, - discoverable: this.discoverable, bot: this.bot, - allow_following_move: this.allowFollowingMove, - hide_follows_count: this.hideFollowsCount, - hide_followers_count: this.hideFollowersCount, show_role: this.showRole /* eslint-enable camelcase */ } }).then((user) => { diff --git a/src/components/settings_modal/tabs/profile_tab.vue b/src/components/settings_modal/tabs/profile_tab.vue index bb3c301d..2cf3e8be 100644 --- a/src/components/settings_modal/tabs/profile_tab.vue +++ b/src/components/settings_modal/tabs/profile_tab.vue @@ -25,61 +25,6 @@ class="bio resize-height" /> -

    - - {{ $t('settings.lock_account_description') }} - -

    -
    - -
    - -
    -
    -

    - - {{ $t('settings.no_rich_text_description') }} - -

    -

    - - {{ $t('settings.hide_follows_description') }} - -

    -

    - - {{ $t('settings.hide_follows_count_description') }} - -

    -

    - - {{ $t('settings.hide_followers_description') }} - -

    -

    - - {{ $t('settings.hide_followers_count_description') }} - -

    -

    - - {{ $t('settings.allow_following_move') }} - -

    -

    - - {{ $t('settings.discoverable') }} - -

    {{ $t('settings.profile_fields.label') }}

    {{ $t('settings.avatar_size_instruction') }}

    -
    - @@ -269,6 +208,67 @@ {{ $t('settings.save') }}
    +
    +

    {{ $t('settings.account_privacy') }}

    +
      +
    • + + {{ $t('settings.lock_account_description') }} + +
    • +
    • + + {{ $t('settings.discoverable') }} + +
    • +
    • + + {{ $t('settings.allow_following_move') }} + +
    • +
    • + + {{ $t('settings.hide_favorites_description') }} + +
    • +
    • + + {{ $t('settings.hide_followers_description') }} + +
        +
      • + + {{ $t('settings.hide_followers_count_description') }} + +
      • +
      +
    • +
    • + + {{ $t('settings.hide_follows_description') }} + +
        +
      • + + {{ $t('settings.hide_follows_count_description') }} + +
      • +
      +
    • +
    +
    diff --git a/src/i18n/en.json b/src/i18n/en.json index 8eb7fcc6..932e8af5 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -259,11 +259,14 @@ }, "settings": { "app_name": "App name", + "expert_mode": "Expert mode", "save": "Save changes", "security": "Security", "setting_changed": "Setting is different from default", + "setting_server_side": "This setting is tied to your profile and affects all sessions and clients", "enter_current_password_to_confirm": "Enter your current password to confirm your identity", "post_look_feel": "Posts Look & Feel", + "mention_links": "Mention links", "mfa": { "otp": "OTP", "setup_otp": "Setup OTP", @@ -400,6 +403,7 @@ "name": "Label", "value": "Content" }, + "account_privacy": "Privacy", "use_contain_fit": "Don't crop the attachment in thumbnails", "name": "Name", "name_bio": "Name & bio", @@ -417,6 +421,7 @@ "no_rich_text_description": "Strip rich text formatting from all posts", "no_blocks": "No blocks", "no_mutes": "No mutes", + "hide_favorites_description": "Don't show list of my favorites (people still get notified)", "hide_follows_description": "Don't show who I'm following", "hide_followers_description": "Don't show who's following me", "hide_follows_count_description": "Don't show follow count", diff --git a/src/main.js b/src/main.js index 3895da89..73bd49ed 100644 --- a/src/main.js +++ b/src/main.js @@ -11,6 +11,7 @@ import statusesModule from './modules/statuses.js' import usersModule from './modules/users.js' import apiModule from './modules/api.js' import configModule from './modules/config.js' +import serverSideConfigModule from './modules/serverSideConfig.js' import shoutModule from './modules/shout.js' import oauthModule from './modules/oauth.js' import authFlowModule from './modules/auth_flow.js' @@ -88,6 +89,7 @@ const persistedStateOptions = { users: usersModule, api: apiModule, config: configModule, + serverSideConfig: serverSideConfigModule, shout: shoutModule, oauth: oauthModule, authFlow: authFlowModule, diff --git a/src/modules/config.js b/src/modules/config.js index 20979174..775ee398 100644 --- a/src/modules/config.js +++ b/src/modules/config.js @@ -16,6 +16,7 @@ export const multiChoiceProperties = [ ] export const defaultState = { + expertLevel: 0, // used to track which settings to show and hide colors: {}, theme: undefined, customTheme: undefined, diff --git a/src/modules/serverSideConfig.js b/src/modules/serverSideConfig.js new file mode 100644 index 00000000..75ea91be --- /dev/null +++ b/src/modules/serverSideConfig.js @@ -0,0 +1,111 @@ +import { get, set } from 'lodash' + +export const settingsMapGet = { + 'defaultScope': 'source.privacy', + 'defaultNSFW': 'source.sensitive', + 'stripRichContent': 'source.pleroma.no_rich_content', + // Privacy + 'locked': 'locked', + 'acceptChatMessages': 'pleroma.accepts_chat_messages', + 'allowFollowingMove': 'pleroma.allow_following_move', + 'discoverable': 'source.discoverable', + 'hideFavorites': 'pleroma.hide_favorites', + 'hideFollowers': 'pleroma.hide_followers', + 'hideFollows': 'pleroma.hide_follows', + 'hideFollowersCount': 'pleroma.hide_followers_count', + 'hideFollowsCount': 'pleroma.hide_follows_count', + // NotificationSettingsAPIs + 'webPushHideContents': 'pleroma.notification_settings.hide_notification_contents', + 'blockNotificationsFromStrangers': 'pleroma.notification_settings.block_from_strangers' +} + +export const settingsMapSet = { + 'defaultScope': 'source.privacy', + 'defaultNSFW': 'source.sensitive', + 'stripRichContent': 'source.pleroma.no_rich_content', + // Privacy + 'locked': 'locked', + 'acceptChatMessages': 'accepts_chat_messages', + 'allowFollowingMove': 'allow_following_move', + 'discoverable': 'source.discoverable', + 'hideFavorites': 'hide_favorites', + 'hideFollowers': 'hide_followers', + 'hideFollows': 'hide_follows', + 'hideFollowersCount': 'hide_followers_count', + 'hideFollowsCount': 'hide_follows_count', + // NotificationSettingsAPIs + 'webPushHideContents': 'hide_notification_contents', + 'blockNotificationsFromStrangers': 'block_from_strangers' +} + +export const customAPIs = { + __defaultApi: 'updateProfile', + 'webPushHideContents': 'updateNotificationSettings', + 'blockNotificationsFromStrangers': 'updateNotificationSettings' +} + +export const defaultState = Object.fromEntries(Object.keys(settingsMapGet).map(key => [key, undefined])) + +const serverSideConfig = { + state: { ...defaultState }, + mutations: { + confirmServerSideOption (state, { name, value }) { + set(state, name, value) + }, + wipeServerSideOption (state, { name }) { + set(state, name, undefined) + }, + // Set the settings based on their path location + setCurrentUser (state, user) { + Object.entries(settingsMapGet).forEach(([name, path]) => { + set(state, name, get(user._original, path)) + }) + } + }, + actions: { + setServerSideOption ({ rootState, state, commit, dispatch }, { name, value }) { + const oldValue = get(state, name) + const params = {} + const path = settingsMapSet[name] + if (!path) throw new Error('Invalid server-side setting') + commit('wipeServerSideOption', { name }) + const customAPIName = customAPIs[name] || customAPIs.__defaultApi + const api = rootState.api.backendInteractor[customAPIName] + let prefix = '' + switch (customAPIName) { + case 'updateNotificationSettings': + prefix = 'settings.' + break + default: + prefix = 'params.' + break + } + + set(params, prefix + path, value) + api(params) + .then((result) => { + switch (customAPIName) { + case 'updateNotificationSettings': + console.log(result) + if (result.status === 'success') { + commit('confirmServerSideOption', { name, value }) + } else { + commit('confirmServerSideOption', { name, value: oldValue }) + } + break + default: + commit('addNewUsers', [result]) + commit('setCurrentUser', result) + break + } + console.log(state) + }) + .catch((e) => { + console.warn('Error setting server-side option:', e) + commit('confirmServerSideOption', { name, value: oldValue }) + }) + } + } +} + +export default serverSideConfig diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js index 7025d803..f219c161 100644 --- a/src/services/entity_normalizer/entity_normalizer.service.js +++ b/src/services/entity_normalizer/entity_normalizer.service.js @@ -44,6 +44,7 @@ export const parseUser = (data) => { const mastoShort = masto && !data.hasOwnProperty('avatar') output.id = String(data.id) + output._original = data // used for server-side settings if (masto) { output.screen_name = data.acct -- cgit v1.2.3-70-g09d2 From f96e5882d16a8662ceb7b8cc6aa36fe131a2682f Mon Sep 17 00:00:00 2001 From: Tusooa Zhu Date: Sun, 1 Aug 2021 13:39:56 -0400 Subject: Make media modal be aware of multi-touch actions Originally the media viewer would think every touch is a swipe (one-finger touch event), so we would encounter the case where a two-finger scale event would incorrectly change the current media. This is now fixed. --- src/components/media_modal/media_modal.js | 25 ++++---- src/components/media_modal/media_modal.vue | 1 + src/services/gesture_service/gesture_service.js | 83 ++++++++++++++++++++++++- 3 files changed, 93 insertions(+), 16 deletions(-) (limited to 'src/services') diff --git a/src/components/media_modal/media_modal.js b/src/components/media_modal/media_modal.js index b8bce730..f3d381ee 100644 --- a/src/components/media_modal/media_modal.js +++ b/src/components/media_modal/media_modal.js @@ -53,28 +53,25 @@ const MediaModal = { } }, created () { - this.mediaSwipeGestureRight = GestureService.swipeGesture( - GestureService.DIRECTION_RIGHT, - this.goPrev, - 50 - ) - this.mediaSwipeGestureLeft = GestureService.swipeGesture( - GestureService.DIRECTION_LEFT, - this.goNext, - 50 - ) + this.mediaGesture = new GestureService.SwipeAndScaleGesture({ + direction: GestureService.DIRECTION_LEFT, + callbackPositive: this.goNext, + callbackNegative: this.goPrev, + threshold: 50 + }) }, methods: { getType (media) { return fileTypeService.fileType(media.mimetype) }, mediaTouchStart (e) { - GestureService.beginSwipe(e, this.mediaSwipeGestureRight) - GestureService.beginSwipe(e, this.mediaSwipeGestureLeft) + this.mediaGesture.start(e) }, mediaTouchMove (e) { - GestureService.updateSwipe(e, this.mediaSwipeGestureRight) - GestureService.updateSwipe(e, this.mediaSwipeGestureLeft) + this.mediaGesture.move(e) + }, + mediaTouchEnd (e) { + this.mediaGesture.end(e) }, hide () { this.$store.dispatch('closeMediaViewer') diff --git a/src/components/media_modal/media_modal.vue b/src/components/media_modal/media_modal.vue index 8680267b..b5a65d50 100644 --- a/src/components/media_modal/media_modal.vue +++ b/src/components/media_modal/media_modal.vue @@ -13,6 +13,7 @@ :title="currentMedia.description" @touchstart.stop="mediaTouchStart" @touchmove.stop="mediaTouchMove" + @touchend.stop="mediaTouchEnd" @click="hide" @load="onImageLoaded" > diff --git a/src/services/gesture_service/gesture_service.js b/src/services/gesture_service/gesture_service.js index 88a328f3..8df4f03d 100644 --- a/src/services/gesture_service/gesture_service.js +++ b/src/services/gesture_service/gesture_service.js @@ -4,9 +4,17 @@ const DIRECTION_RIGHT = [1, 0] const DIRECTION_UP = [0, -1] const DIRECTION_DOWN = [0, 1] +const isSwipeEvent = e => (e.touches.length === 1) +const isSwipeEventEnd = e => (e.changedTouches.length === 1) + +const isScaleEvent = e => (e.targetTouches.length === 2) +// const isScaleEventEnd = e => (e.changedTouches.length === 2) + const deltaCoord = (oldCoord, newCoord) => [newCoord[0] - oldCoord[0], newCoord[1] - oldCoord[1]] -const touchEventCoord = e => ([e.touches[0].screenX, e.touches[0].screenY]) +const touchCoord = touch => [touch.screenX, touch.screenY] + +const touchEventCoord = e => touchCoord(e.touches[0]) const vectorLength = v => Math.sqrt(v[0] * v[0] + v[1] * v[1]) @@ -61,6 +69,76 @@ const updateSwipe = (event, gesture) => { gesture._swiping = false } +class SwipeAndScaleGesture { + constructor ({ + direction, callbackPositive, callbackNegative, + previewCallback, threshold = 30, perpendicularTolerance = 1.0 + }) { + this.direction = direction + this.previewCallback = previewCallback + this.callbackPositive = callbackPositive + this.callbackNegative = callbackNegative + this.threshold = threshold + this.perpendicularTolerance = perpendicularTolerance + this._startPos = [0, 0] + this._swiping = false + } + + start (event) { + console.log('start() called', event) + if (isSwipeEvent(event)) { + this._startPos = touchEventCoord(event) + console.log('start pos:', this._startPos) + this._swiping = true + } else if (isScaleEvent(event)) { + this._scalePoints = [...event.targetTouches] + this._swiping = false + } + } + + move (event) { + if (isScaleEvent(event)) { + } + } + + end (event) { + console.log('end() called', event) + if (!isSwipeEventEnd(event)) { + console.log('not swipe event') + return + } + if (!this._swiping) { + console.log('not swiping') + return + } + this.swiping = false + + console.log('is swipe event') + + // movement too small + const touch = event.changedTouches[0] + const delta = deltaCoord(this._startPos, touchCoord(touch)) + if (vectorLength(delta) < this.threshold) return + // movement is opposite from direction + const isPositive = dotProduct(delta, this.direction) > 0 + + // movement perpendicular to direction is too much + const towardsDir = project(delta, this.direction) + const perpendicularDir = perpendicular(this.direction) + const towardsPerpendicular = project(delta, perpendicularDir) + if ( + vectorLength(towardsDir) * this.perpendicularTolerance < + vectorLength(towardsPerpendicular) + ) return + + if (isPositive) { + this.callbackPositive() + } else { + this.callbackNegative() + } + } +} + const GestureService = { DIRECTION_LEFT, DIRECTION_RIGHT, @@ -68,7 +146,8 @@ const GestureService = { DIRECTION_DOWN, swipeGesture, beginSwipe, - updateSwipe + updateSwipe, + SwipeAndScaleGesture } export default GestureService -- cgit v1.2.3-70-g09d2 From a7570f5eb2b8bc576edbcc8e212b2c873ac99e7e Mon Sep 17 00:00:00 2001 From: Tusooa Zhu Date: Sun, 1 Aug 2021 19:46:27 -0400 Subject: Preview swipe action --- src/components/media_modal/media_modal.js | 26 +++++++++ src/components/media_modal/media_modal.vue | 1 + src/modules/media_viewer.js | 64 +++++++++++++++++++++- src/services/gesture_service/gesture_service.js | 73 +++++++++++++++++-------- 4 files changed, 138 insertions(+), 26 deletions(-) (limited to 'src/services') diff --git a/src/components/media_modal/media_modal.js b/src/components/media_modal/media_modal.js index f3d381ee..8f67db2b 100644 --- a/src/components/media_modal/media_modal.js +++ b/src/components/media_modal/media_modal.js @@ -4,6 +4,7 @@ import Modal from '../modal/modal.vue' import fileTypeService from '../../services/file_type/file_type.service.js' import GestureService from '../../services/gesture_service/gesture_service' import Flash from 'src/components/flash/flash.vue' +import Vuex from 'vuex' import { library } from '@fortawesome/fontawesome-svg-core' import { faChevronLeft, @@ -17,6 +18,8 @@ library.add( faCircleNotch ) +const onlyXAxis = ([x, y]) => [x, 0] + const MediaModal = { components: { StillImage, @@ -50,6 +53,15 @@ const MediaModal = { }, type () { return this.currentMedia ? this.getType(this.currentMedia) : null + }, + scaling () { + return this.$store.state.mediaViewer.swipeScaler.scaling + }, + offsets () { + return this.$store.state.mediaViewer.swipeScaler.offsets + }, + transform () { + return `scale(${this.scaling}, ${this.scaling}) translate(${this.offsets[0]}px, ${this.offsets[1]}px)` } }, created () { @@ -57,6 +69,8 @@ const MediaModal = { direction: GestureService.DIRECTION_LEFT, callbackPositive: this.goNext, callbackNegative: this.goPrev, + swipePreviewCallback: this.handleSwipePreview, + swipeEndCallback: this.handleSwipeEnd, threshold: 50 }) }, @@ -99,6 +113,18 @@ const MediaModal = { onImageLoaded () { this.loading = false }, + handleSwipePreview (offsets) { + this.$store.dispatch('swipeScaler/apply', { offsets: onlyXAxis(offsets) }) + }, + handleSwipeEnd (sign) { + if (sign === 0) { + this.$store.dispatch('swipeScaler/revert') + } else if (sign > 0) { + this.goNext() + } else { + this.goPrev() + } + }, handleKeyupEvent (e) { if (this.showing && e.keyCode === 27) { // escape this.hide() diff --git a/src/components/media_modal/media_modal.vue b/src/components/media_modal/media_modal.vue index b5a65d50..728c3035 100644 --- a/src/components/media_modal/media_modal.vue +++ b/src/components/media_modal/media_modal.vue @@ -11,6 +11,7 @@ :src="currentMedia.url" :alt="currentMedia.description" :title="currentMedia.description" + :style="{ transform }" @touchstart.stop="mediaTouchStart" @touchmove.stop="mediaTouchMove" @touchend.stop="mediaTouchEnd" diff --git a/src/modules/media_viewer.js b/src/modules/media_viewer.js index ebcba01d..38723763 100644 --- a/src/modules/media_viewer.js +++ b/src/modules/media_viewer.js @@ -20,19 +20,79 @@ const mediaViewer = { } }, actions: { - setMedia ({ commit }, attachments) { + setMedia ({ commit, dispatch }, attachments) { const media = attachments.filter(attachment => { const type = fileTypeService.fileType(attachment.mimetype) return supportedTypes.has(type) }) commit('setMedia', media) + dispatch('swipeScaler/reset') }, setCurrentMedia ({ commit, state }, current) { const index = state.media.indexOf(current) commit('setCurrentMedia', index || 0) + dispatch('swipeScaler/reset') }, - closeMediaViewer ({ commit }) { + closeMediaViewer ({ commit, dispatch }) { commit('close') + dispatch('swipeScaler/reset') + } + }, + modules: { + swipeScaler: { + namespaced: true, + + state: { + origOffsets: [0, 0], + offsets: [0, 0], + origScaling: 1, + scaling: 1 + }, + + mutations: { + reset (state) { + state.origOffsets = [0, 0] + state.offsets = [0, 0] + state.origScaling = 1 + state.scaling = 1 + }, + applyOffsets (state, { offsets }) { + state.offsets = state.origOffsets.map((k, n) => k + offsets[n]) + }, + applyScaling (state, { scaling }) { + state.scaling = state.origScaling * scaling + }, + finishOffsets (state) { + state.origOffsets = [...state.offsets] + }, + finishScaling (state) { + state.origScaling = state.scaling + }, + revertOffsets (state) { + state.offsets = [...state.origOffsets] + }, + revertScaling (state) { + state.scaling = state.origScaling + } + }, + + actions: { + reset ({ commit }) { + commit('reset') + }, + apply ({ commit }, { offsets, scaling = 1 }) { + commit('applyOffsets', { offsets }) + commit('applyScaling', { scaling }) + }, + finish ({ commit }) { + commit('finishOffsets') + commit('finishScaling') + }, + revert ({ commit }) { + commit('revertOffsets') + commit('revertScaling') + } + } } } } diff --git a/src/services/gesture_service/gesture_service.js b/src/services/gesture_service/gesture_service.js index 8df4f03d..8f406762 100644 --- a/src/services/gesture_service/gesture_service.js +++ b/src/services/gesture_service/gesture_service.js @@ -70,14 +70,28 @@ const updateSwipe = (event, gesture) => { } class SwipeAndScaleGesture { + // swipePreviewCallback(offsets: Array[Number]) + // offsets: the offset vector which the underlying component should move, from the starting position + // pinchPreviewCallback(offsets: Array[Number], scaling: Number) + // offsets: the offset vector which the underlying component should move, from the starting position + // scaling: the scaling factor we should apply to the underlying component, from the starting position + // swipeEndcallback(sign: 0|-1|1) + // sign: if the swipe does not meet the threshold, 0 + // if the swipe meets the threshold in the positive direction, 1 + // if the swipe meets the threshold in the negative direction, -1 constructor ({ - direction, callbackPositive, callbackNegative, - previewCallback, threshold = 30, perpendicularTolerance = 1.0 + direction, + // swipeStartCallback, pinchStartCallback, + swipePreviewCallback, pinchPreviewCallback, + swipeEndCallback, pinchEndCallback, + threshold = 30, perpendicularTolerance = 1.0 }) { + const nop = () => {} this.direction = direction - this.previewCallback = previewCallback - this.callbackPositive = callbackPositive - this.callbackNegative = callbackNegative + this.swipePreviewCallback = swipePreviewCallback || nop + this.pinchPreviewCallback = pinchPreviewCallback || nop + this.swipeEndCallback = swipeEndCallback || nop + this.pinchEndCallback = pinchEndCallback || nop this.threshold = threshold this.perpendicularTolerance = perpendicularTolerance this._startPos = [0, 0] @@ -97,7 +111,12 @@ class SwipeAndScaleGesture { } move (event) { - if (isScaleEvent(event)) { + if (isSwipeEvent(event)) { + const touch = event.changedTouches[0] + const delta = deltaCoord(this._startPos, touchCoord(touch)) + + this.swipePreviewCallback(delta) + } else if (isScaleEvent(event)) { } } @@ -118,24 +137,30 @@ class SwipeAndScaleGesture { // movement too small const touch = event.changedTouches[0] const delta = deltaCoord(this._startPos, touchCoord(touch)) - if (vectorLength(delta) < this.threshold) return - // movement is opposite from direction - const isPositive = dotProduct(delta, this.direction) > 0 - - // movement perpendicular to direction is too much - const towardsDir = project(delta, this.direction) - const perpendicularDir = perpendicular(this.direction) - const towardsPerpendicular = project(delta, perpendicularDir) - if ( - vectorLength(towardsDir) * this.perpendicularTolerance < - vectorLength(towardsPerpendicular) - ) return - - if (isPositive) { - this.callbackPositive() - } else { - this.callbackNegative() - } + this.swipePreviewCallback(delta) + + const sign = (() => { + if (vectorLength(delta) < this.threshold) { + return 0 + } + // movement is opposite from direction + const isPositive = dotProduct(delta, this.direction) > 0 + + // movement perpendicular to direction is too much + const towardsDir = project(delta, this.direction) + const perpendicularDir = perpendicular(this.direction) + const towardsPerpendicular = project(delta, perpendicularDir) + if ( + vectorLength(towardsDir) * this.perpendicularTolerance < + vectorLength(towardsPerpendicular) + ) { + return 0 + } + + return isPositive ? 1 : -1 + })() + + this.swipeEndCallback(sign) } } -- cgit v1.2.3-70-g09d2 From d9030b4fddc9f47cb2f16a7a65f57a5703059e3f Mon Sep 17 00:00:00 2001 From: Tusooa Zhu Date: Sun, 1 Aug 2021 21:39:07 -0400 Subject: Handle pinch action --- src/components/media_modal/media_modal.js | 11 ++++++- src/components/media_modal/media_modal.vue | 6 ++-- src/modules/media_viewer.js | 14 +++------ src/services/gesture_service/gesture_service.js | 42 +++++++++++++++++++++++-- 4 files changed, 57 insertions(+), 16 deletions(-) (limited to 'src/services') diff --git a/src/components/media_modal/media_modal.js b/src/components/media_modal/media_modal.js index 8f67db2b..04dd8658 100644 --- a/src/components/media_modal/media_modal.js +++ b/src/components/media_modal/media_modal.js @@ -61,7 +61,7 @@ const MediaModal = { return this.$store.state.mediaViewer.swipeScaler.offsets }, transform () { - return `scale(${this.scaling}, ${this.scaling}) translate(${this.offsets[0]}px, ${this.offsets[1]}px)` + return `translate(${this.offsets[0]}px, ${this.offsets[1]}px) scale(${this.scaling}, ${this.scaling})` } }, created () { @@ -71,6 +71,8 @@ const MediaModal = { callbackNegative: this.goPrev, swipePreviewCallback: this.handleSwipePreview, swipeEndCallback: this.handleSwipeEnd, + pinchPreviewCallback: this.handlePinchPreview, + pinchEndCallback: this.handlePinchEnd, threshold: 50 }) }, @@ -125,6 +127,13 @@ const MediaModal = { this.goPrev() } }, + handlePinchPreview (offsets, scaling) { + console.log('handle pinch preview:', offsets, scaling) + this.$store.dispatch('swipeScaler/apply', { offsets, scaling }) + }, + handlePinchEnd () { + this.$store.dispatch('swipeScaler/finish') + }, handleKeyupEvent (e) { if (this.showing && e.keyCode === 27) { // escape this.hide() diff --git a/src/components/media_modal/media_modal.vue b/src/components/media_modal/media_modal.vue index 728c3035..853dec1d 100644 --- a/src/components/media_modal/media_modal.vue +++ b/src/components/media_modal/media_modal.vue @@ -12,9 +12,9 @@ :alt="currentMedia.description" :title="currentMedia.description" :style="{ transform }" - @touchstart.stop="mediaTouchStart" - @touchmove.stop="mediaTouchMove" - @touchend.stop="mediaTouchEnd" + @touchstart.stop.prevent="mediaTouchStart" + @touchmove.stop.prevent="mediaTouchMove" + @touchend.stop.prevent="mediaTouchEnd" @click="hide" @load="onImageLoaded" > diff --git a/src/modules/media_viewer.js b/src/modules/media_viewer.js index 38723763..d62aa7c1 100644 --- a/src/modules/media_viewer.js +++ b/src/modules/media_viewer.js @@ -62,16 +62,12 @@ const mediaViewer = { applyScaling (state, { scaling }) { state.scaling = state.origScaling * scaling }, - finishOffsets (state) { + finish (state) { state.origOffsets = [...state.offsets] - }, - finishScaling (state) { state.origScaling = state.scaling }, - revertOffsets (state) { + revert (state) { state.offsets = [...state.origOffsets] - }, - revertScaling (state) { state.scaling = state.origScaling } }, @@ -85,12 +81,10 @@ const mediaViewer = { commit('applyScaling', { scaling }) }, finish ({ commit }) { - commit('finishOffsets') - commit('finishScaling') + commit('finish') }, revert ({ commit }) { - commit('revertOffsets') - commit('revertScaling') + commit('revert') } } } diff --git a/src/services/gesture_service/gesture_service.js b/src/services/gesture_service/gesture_service.js index 8f406762..82337bc6 100644 --- a/src/services/gesture_service/gesture_service.js +++ b/src/services/gesture_service/gesture_service.js @@ -4,14 +4,21 @@ const DIRECTION_RIGHT = [1, 0] const DIRECTION_UP = [0, -1] const DIRECTION_DOWN = [0, 1] +const DISTANCE_MIN = 1 + const isSwipeEvent = e => (e.touches.length === 1) const isSwipeEventEnd = e => (e.changedTouches.length === 1) const isScaleEvent = e => (e.targetTouches.length === 2) -// const isScaleEventEnd = e => (e.changedTouches.length === 2) +const isScaleEventEnd = e => (e.targetTouches.length === 1) const deltaCoord = (oldCoord, newCoord) => [newCoord[0] - oldCoord[0], newCoord[1] - oldCoord[1]] +const vectorMinus = (a, b) => a.map((k, n) => k - b[n]) +const vectorAdd = (a, b) => a.map((k, n) => k + b[n]) + +const avgCoord = (coords) => [...coords].reduce(vectorAdd, [0, 0]).map(d => d / coords.length) + const touchCoord = touch => [touch.screenX, touch.screenY] const touchEventCoord = e => touchCoord(e.touches[0]) @@ -22,6 +29,8 @@ const perpendicular = v => [v[1], -v[0]] const dotProduct = (v1, v2) => v1[0] * v2[0] + v1[1] * v2[1] +// const numProduct = (num, v) => v.map(k => num * k) + const project = (v1, v2) => { const scalar = (dotProduct(v1, v2) / dotProduct(v2, v2)) return [scalar * v2[0], scalar * v2[1]] @@ -86,7 +95,7 @@ class SwipeAndScaleGesture { swipeEndCallback, pinchEndCallback, threshold = 30, perpendicularTolerance = 1.0 }) { - const nop = () => {} + const nop = () => { console.log('Warning: Not implemented') } this.direction = direction this.swipePreviewCallback = swipePreviewCallback || nop this.pinchPreviewCallback = pinchPreviewCallback || nop @@ -95,6 +104,7 @@ class SwipeAndScaleGesture { this.threshold = threshold this.perpendicularTolerance = perpendicularTolerance this._startPos = [0, 0] + this._startDistance = DISTANCE_MIN this._swiping = false } @@ -105,23 +115,51 @@ class SwipeAndScaleGesture { console.log('start pos:', this._startPos) this._swiping = true } else if (isScaleEvent(event)) { + const coords = [...event.targetTouches].map(touchCoord) + this._startPos = avgCoord(coords) + this._startDistance = vectorLength(deltaCoord(coords[0], coords[1])) + if (this._startDistance < DISTANCE_MIN) { + this._startDistance = DISTANCE_MIN + } this._scalePoints = [...event.targetTouches] this._swiping = false + console.log( + 'is scale event, start =', this._startPos, + 'dist =', this._startDistance) } } move (event) { + // console.log('move called', event) if (isSwipeEvent(event)) { const touch = event.changedTouches[0] const delta = deltaCoord(this._startPos, touchCoord(touch)) this.swipePreviewCallback(delta) } else if (isScaleEvent(event)) { + console.log('is scale event') + const coords = [...event.targetTouches].map(touchCoord) + const curPos = avgCoord(coords) + const curDistance = vectorLength(deltaCoord(coords[0], coords[1])) + const scaling = curDistance / this._startDistance + const posDiff = vectorMinus(curPos, this._startPos) + // const delta = vectorAdd(numProduct((1 - scaling), this._startPos), posDiff) + const delta = posDiff + // console.log( + // 'is scale event, cur =', curPos, + // 'dist =', curDistance, + // 'scale =', scaling, + // 'delta =', delta) + this.pinchPreviewCallback(delta, scaling) } } end (event) { console.log('end() called', event) + if (isScaleEventEnd(event)) { + this.pinchEndCallback() + } + if (!isSwipeEventEnd(event)) { console.log('not swipe event') return -- cgit v1.2.3-70-g09d2 From 29cd8fbd3bd9f936d2639a2edd773e1245b0b5a5 Mon Sep 17 00:00:00 2001 From: Tusooa Zhu Date: Mon, 2 Aug 2021 19:11:59 -0400 Subject: Add swipe-click handler to media modal Now swiping will correctly change the current media, and with a good preview. Clicking without swiping closes the overlay. --- src/components/media_modal/media_modal.js | 58 ++++------- src/components/media_modal/media_modal.vue | 13 ++- src/components/pinch_zoom/pinch_zoom.js | 5 + src/modules/media_viewer.js | 56 +--------- src/services/gesture_service/gesture_service.js | 130 +++++++++++------------- 5 files changed, 94 insertions(+), 168 deletions(-) (limited to 'src/services') diff --git a/src/components/media_modal/media_modal.js b/src/components/media_modal/media_modal.js index 684fbe45..b4c0cfb4 100644 --- a/src/components/media_modal/media_modal.js +++ b/src/components/media_modal/media_modal.js @@ -2,10 +2,11 @@ import StillImage from '../still-image/still-image.vue' import VideoAttachment from '../video_attachment/video_attachment.vue' import Modal from '../modal/modal.vue' import PinchZoom from '../pinch_zoom/pinch_zoom.vue' -import fileTypeService from '../../services/file_type/file_type.service.js' +import SwipeClick from '../swipe_click/swipe_click.vue' import GestureService from '../../services/gesture_service/gesture_service' import Flash from 'src/components/flash/flash.vue' import Vuex from 'vuex' +import fileTypeService from '../../services/file_type/file_type.service.js' import { library } from '@fortawesome/fontawesome-svg-core' import { faChevronLeft, @@ -28,13 +29,15 @@ const MediaModal = { StillImage, VideoAttachment, PinchZoom, + SwipeClick, Modal, Flash }, data () { return { loading: false, - pinchZoomOptions: {} + swipeDirection: GestureService.DIRECTION_LEFT, + swipeThreshold: 50 } }, computed: { @@ -70,30 +73,19 @@ const MediaModal = { } }, created () { - this.mediaGesture = new GestureService.SwipeAndScaleGesture({ - direction: GestureService.DIRECTION_LEFT, - callbackPositive: this.goNext, - callbackNegative: this.goPrev, - swipePreviewCallback: this.handleSwipePreview, - swipeEndCallback: this.handleSwipeEnd, - pinchPreviewCallback: this.handlePinchPreview, - pinchEndCallback: this.handlePinchEnd, - threshold: 50 - }) + // this.mediaGesture = new GestureService.SwipeAndScaleGesture({ + // callbackPositive: this.goNext, + // callbackNegative: this.goPrev, + // swipePreviewCallback: this.handleSwipePreview, + // swipeEndCallback: this.handleSwipeEnd, + // pinchPreviewCallback: this.handlePinchPreview, + // pinchEndCallback: this.handlePinchEnd + // }) }, methods: { getType (media) { return fileTypeService.fileType(media.mimetype) }, - mediaTouchStart (e) { - this.mediaGesture.start(e) - }, - mediaTouchMove (e) { - this.mediaGesture.move(e) - }, - mediaTouchEnd (e) { - this.mediaGesture.end(e) - }, hide () { this.$store.dispatch('closeMediaViewer') }, @@ -105,6 +97,7 @@ const MediaModal = { this.loading = true } this.$store.dispatch('setCurrentMedia', newMedia) + this.$refs.pinchZoom.setTransform({ scale: 1, x: 0, y: 0 }) } }, goNext () { @@ -115,40 +108,25 @@ const MediaModal = { this.loading = true } this.$store.dispatch('setCurrentMedia', newMedia) + this.$refs.pinchZoom.setTransform({ scale: 1, x: 0, y: 0 }) } }, onImageLoaded () { this.loading = false }, handleSwipePreview (offsets) { - this.$store.dispatch('swipeScaler/apply', { - offsets: this.scaling > SCALING_ENABLE_MOVE_THRESHOLD ? offsets : onlyXAxis(offsets) - }) + this.$refs.pinchZoom.setTransform({ scale: 1, x: offsets[0], y: 0 }) }, handleSwipeEnd (sign) { - if (this.scaling > SCALING_ENABLE_MOVE_THRESHOLD) { - this.$store.dispatch('swipeScaler/finish') - return - } + console.log('handleSwipeEnd:', sign) if (sign === 0) { - this.$store.dispatch('swipeScaler/reset') + this.$refs.pinchZoom.setTransform({ scale: 1, x: 0, y: 0 }) } else if (sign > 0) { this.goNext() } else { this.goPrev() } }, - handlePinchPreview (offsets, scaling) { - console.log('handle pinch preview:', offsets, scaling) - this.$store.dispatch('swipeScaler/apply', { offsets, scaling }) - }, - handlePinchEnd () { - if (this.scaling > SCALING_RESET_MIN) { - this.$store.dispatch('swipeScaler/finish') - } else { - this.$store.dispatch('swipeScaler/reset') - } - }, handleKeyupEvent (e) { if (this.showing && e.keyCode === 27) { // escape this.hide() diff --git a/src/components/media_modal/media_modal.vue b/src/components/media_modal/media_modal.vue index a3fad4c5..e385024e 100644 --- a/src/components/media_modal/media_modal.vue +++ b/src/components/media_modal/media_modal.vue @@ -4,9 +4,16 @@ class="media-modal-view" @backdropClicked="hide" > - + k + offsets[n]) - }, - applyScaling (state, { scaling }) { - state.scaling = state.origScaling * scaling - }, - finish (state) { - state.origOffsets = [...state.offsets] - state.origScaling = state.scaling - }, - revert (state) { - state.offsets = [...state.origOffsets] - state.scaling = state.origScaling - } - }, - - actions: { - reset ({ commit }) { - commit('reset') - }, - apply ({ commit }, { offsets, scaling = 1 }) { - commit('applyOffsets', { offsets }) - commit('applyScaling', { scaling }) - }, - finish ({ commit }) { - commit('finish') - }, - revert ({ commit }) { - commit('revert') - } - } } } } diff --git a/src/services/gesture_service/gesture_service.js b/src/services/gesture_service/gesture_service.js index 82337bc6..f10dec3a 100644 --- a/src/services/gesture_service/gesture_service.js +++ b/src/services/gesture_service/gesture_service.js @@ -4,25 +4,27 @@ const DIRECTION_RIGHT = [1, 0] const DIRECTION_UP = [0, -1] const DIRECTION_DOWN = [0, 1] -const DISTANCE_MIN = 1 +// const DISTANCE_MIN = 1 -const isSwipeEvent = e => (e.touches.length === 1) -const isSwipeEventEnd = e => (e.changedTouches.length === 1) +// const isSwipeEvent = e => (e.touches.length === 1) +// const isSwipeEventEnd = e => (e.changedTouches.length === 1) -const isScaleEvent = e => (e.targetTouches.length === 2) -const isScaleEventEnd = e => (e.targetTouches.length === 1) +// const isScaleEvent = e => (e.targetTouches.length === 2) +// const isScaleEventEnd = e => (e.targetTouches.length === 1) const deltaCoord = (oldCoord, newCoord) => [newCoord[0] - oldCoord[0], newCoord[1] - oldCoord[1]] -const vectorMinus = (a, b) => a.map((k, n) => k - b[n]) -const vectorAdd = (a, b) => a.map((k, n) => k + b[n]) +// const vectorMinus = (a, b) => a.map((k, n) => k - b[n]) +// const vectorAdd = (a, b) => a.map((k, n) => k + b[n]) -const avgCoord = (coords) => [...coords].reduce(vectorAdd, [0, 0]).map(d => d / coords.length) +// const avgCoord = (coords) => [...coords].reduce(vectorAdd, [0, 0]).map(d => d / coords.length) const touchCoord = touch => [touch.screenX, touch.screenY] const touchEventCoord = e => touchCoord(e.touches[0]) +const pointerEventCoord = e => [e.clientX, e.clientY] + const vectorLength = v => Math.sqrt(v[0] * v[0] + v[1] * v[1]) const perpendicular = v => [v[1], -v[0]] @@ -78,104 +80,87 @@ const updateSwipe = (event, gesture) => { gesture._swiping = false } -class SwipeAndScaleGesture { +class SwipeAndClickGesture { // swipePreviewCallback(offsets: Array[Number]) // offsets: the offset vector which the underlying component should move, from the starting position - // pinchPreviewCallback(offsets: Array[Number], scaling: Number) - // offsets: the offset vector which the underlying component should move, from the starting position - // scaling: the scaling factor we should apply to the underlying component, from the starting position - // swipeEndcallback(sign: 0|-1|1) + // swipeEndCallback(sign: 0|-1|1) // sign: if the swipe does not meet the threshold, 0 // if the swipe meets the threshold in the positive direction, 1 // if the swipe meets the threshold in the negative direction, -1 constructor ({ direction, - // swipeStartCallback, pinchStartCallback, - swipePreviewCallback, pinchPreviewCallback, - swipeEndCallback, pinchEndCallback, + // swipeStartCallback + swipePreviewCallback, + swipeEndCallback, + swipeCancelCallback, + swipelessClickCallback, threshold = 30, perpendicularTolerance = 1.0 }) { const nop = () => { console.log('Warning: Not implemented') } this.direction = direction this.swipePreviewCallback = swipePreviewCallback || nop - this.pinchPreviewCallback = pinchPreviewCallback || nop this.swipeEndCallback = swipeEndCallback || nop - this.pinchEndCallback = pinchEndCallback || nop + this.swipeCancelCallback = swipeCancelCallback || nop + this.swipelessClickCallback = swipelessClickCallback || nop this.threshold = threshold this.perpendicularTolerance = perpendicularTolerance + this._reset() + } + + _reset () { this._startPos = [0, 0] - this._startDistance = DISTANCE_MIN + this._pointerId = -1 this._swiping = false + this._swiped = false } start (event) { console.log('start() called', event) - if (isSwipeEvent(event)) { - this._startPos = touchEventCoord(event) - console.log('start pos:', this._startPos) - this._swiping = true - } else if (isScaleEvent(event)) { - const coords = [...event.targetTouches].map(touchCoord) - this._startPos = avgCoord(coords) - this._startDistance = vectorLength(deltaCoord(coords[0], coords[1])) - if (this._startDistance < DISTANCE_MIN) { - this._startDistance = DISTANCE_MIN - } - this._scalePoints = [...event.targetTouches] - this._swiping = false - console.log( - 'is scale event, start =', this._startPos, - 'dist =', this._startDistance) - } + + this._startPos = pointerEventCoord(event) + this._pointerId = event.pointerId + console.log('start pos:', this._startPos) + this._swiping = true + this._swiped = false } move (event) { - // console.log('move called', event) - if (isSwipeEvent(event)) { - const touch = event.changedTouches[0] - const delta = deltaCoord(this._startPos, touchCoord(touch)) + if (this._swiping && this._pointerId === event.pointerId) { + this._swiped = true + + const coord = pointerEventCoord(event) + const delta = deltaCoord(this._startPos, coord) this.swipePreviewCallback(delta) - } else if (isScaleEvent(event)) { - console.log('is scale event') - const coords = [...event.targetTouches].map(touchCoord) - const curPos = avgCoord(coords) - const curDistance = vectorLength(deltaCoord(coords[0], coords[1])) - const scaling = curDistance / this._startDistance - const posDiff = vectorMinus(curPos, this._startPos) - // const delta = vectorAdd(numProduct((1 - scaling), this._startPos), posDiff) - const delta = posDiff - // console.log( - // 'is scale event, cur =', curPos, - // 'dist =', curDistance, - // 'scale =', scaling, - // 'delta =', delta) - this.pinchPreviewCallback(delta, scaling) } } - end (event) { - console.log('end() called', event) - if (isScaleEventEnd(event)) { - this.pinchEndCallback() - } - - if (!isSwipeEventEnd(event)) { - console.log('not swipe event') + cancel (event) { + if (!this._swiping || this._pointerId !== event.pointerId) { return } + + this.swipeCancelCallback() + } + + end (event) { if (!this._swiping) { console.log('not swiping') return } - this.swiping = false - console.log('is swipe event') + if (this._pointerId !== event.pointerId) { + console.log('pointer id does not match') + return + } + + this._swiping = false + + console.log('end: is swipe event') // movement too small - const touch = event.changedTouches[0] - const delta = deltaCoord(this._startPos, touchCoord(touch)) - this.swipePreviewCallback(delta) + const coord = pointerEventCoord(event) + const delta = deltaCoord(this._startPos, coord) const sign = (() => { if (vectorLength(delta) < this.threshold) { @@ -198,7 +183,12 @@ class SwipeAndScaleGesture { return isPositive ? 1 : -1 })() - this.swipeEndCallback(sign) + if (this._swiped) { + this.swipeEndCallback(sign) + } else { + this.swipelessClickCallback() + } + this._reset() } } @@ -210,7 +200,7 @@ const GestureService = { swipeGesture, beginSwipe, updateSwipe, - SwipeAndScaleGesture + SwipeAndClickGesture } export default GestureService -- cgit v1.2.3-70-g09d2 From 23a6b86ef3c976509bad4fb4f9a223a5724ec7e5 Mon Sep 17 00:00:00 2001 From: Tusooa Zhu Date: Mon, 2 Aug 2021 19:21:18 -0400 Subject: Clean up --- src/components/media_modal/media_modal.js | 23 +++---------------- src/components/media_modal/media_modal.vue | 7 +++--- src/services/gesture_service/gesture_service.js | 30 ++++++++----------------- 3 files changed, 15 insertions(+), 45 deletions(-) (limited to 'src/services') diff --git a/src/components/media_modal/media_modal.js b/src/components/media_modal/media_modal.js index b4c0cfb4..2f00f5f9 100644 --- a/src/components/media_modal/media_modal.js +++ b/src/components/media_modal/media_modal.js @@ -37,7 +37,9 @@ const MediaModal = { return { loading: false, swipeDirection: GestureService.DIRECTION_LEFT, - swipeThreshold: 50 + swipeThreshold: 50, + pinchZoomMinScale: 1, + pinchZoomScaleResetLimit: 1.2 } }, computed: { @@ -62,25 +64,6 @@ const MediaModal = { type () { return this.currentMedia ? this.getType(this.currentMedia) : null }, - scaling () { - return this.$store.state.mediaViewer.swipeScaler.scaling - }, - offsets () { - return this.$store.state.mediaViewer.swipeScaler.offsets - }, - transform () { - return `translate(${this.offsets[0]}px, ${this.offsets[1]}px) scale(${this.scaling}, ${this.scaling})` - } - }, - created () { - // this.mediaGesture = new GestureService.SwipeAndScaleGesture({ - // callbackPositive: this.goNext, - // callbackNegative: this.goPrev, - // swipePreviewCallback: this.handleSwipePreview, - // swipeEndCallback: this.handleSwipeEnd, - // pinchPreviewCallback: this.handlePinchPreview, - // pinchEndCallback: this.handlePinchEnd - // }) }, methods: { getType (media) { diff --git a/src/components/media_modal/media_modal.vue b/src/components/media_modal/media_modal.vue index e385024e..76cf4319 100644 --- a/src/components/media_modal/media_modal.vue +++ b/src/components/media_modal/media_modal.vue @@ -16,12 +16,11 @@ ref="pinchZoom" class="modal-image-container-inner" selector=".modal-image" - allow-pan-min-scale="1" - min-scale="1" - reset-to-min-scale-limit="1.2" reach-min-scale-strategy="reset" stop-propagate-handled="stop-propgate-handled" - :inner-class="'modal-image-container-inner'" + :allow-pan-min-scale="pinchZoomMinScale" + :min-scale="pinchZoomMinScale" + :reset-to-min-scale-limit="pinchZoomScaleResetLimit" > (e.touches.length === 1) -// const isSwipeEventEnd = e => (e.changedTouches.length === 1) - -// const isScaleEvent = e => (e.targetTouches.length === 2) -// const isScaleEventEnd = e => (e.targetTouches.length === 1) - const deltaCoord = (oldCoord, newCoord) => [newCoord[0] - oldCoord[0], newCoord[1] - oldCoord[1]] -// const vectorMinus = (a, b) => a.map((k, n) => k - b[n]) -// const vectorAdd = (a, b) => a.map((k, n) => k + b[n]) - -// const avgCoord = (coords) => [...coords].reduce(vectorAdd, [0, 0]).map(d => d / coords.length) - const touchCoord = touch => [touch.screenX, touch.screenY] const touchEventCoord = e => touchCoord(e.touches[0]) @@ -31,13 +18,14 @@ const perpendicular = v => [v[1], -v[0]] const dotProduct = (v1, v2) => v1[0] * v2[0] + v1[1] * v2[1] -// const numProduct = (num, v) => v.map(k => num * k) - const project = (v1, v2) => { const scalar = (dotProduct(v1, v2) / dotProduct(v2, v2)) return [scalar * v2[0], scalar * v2[1]] } +// const debug = console.log +const debug = () => {} + // direction: either use the constants above or an arbitrary 2d vector. // threshold: how many Px to move from touch origin before checking if the // callback should be called. @@ -96,7 +84,7 @@ class SwipeAndClickGesture { swipelessClickCallback, threshold = 30, perpendicularTolerance = 1.0 }) { - const nop = () => { console.log('Warning: Not implemented') } + const nop = () => { debug('Warning: Not implemented') } this.direction = direction this.swipePreviewCallback = swipePreviewCallback || nop this.swipeEndCallback = swipeEndCallback || nop @@ -115,11 +103,11 @@ class SwipeAndClickGesture { } start (event) { - console.log('start() called', event) + debug('start() called', event) this._startPos = pointerEventCoord(event) this._pointerId = event.pointerId - console.log('start pos:', this._startPos) + debug('start pos:', this._startPos) this._swiping = true this._swiped = false } @@ -145,18 +133,18 @@ class SwipeAndClickGesture { end (event) { if (!this._swiping) { - console.log('not swiping') + debug('not swiping') return } if (this._pointerId !== event.pointerId) { - console.log('pointer id does not match') + debug('pointer id does not match') return } this._swiping = false - console.log('end: is swipe event') + debug('end: is swipe event') // movement too small const coord = pointerEventCoord(event) -- cgit v1.2.3-70-g09d2 From 9f3a983fef9dfedef8f44855e1f939ea944cd0ba Mon Sep 17 00:00:00 2001 From: Tusooa Zhu Date: Mon, 2 Aug 2021 20:32:02 -0400 Subject: Use native click for hiding overlay The pointerup strategy is unsuccessful, as some other overlays (Firefox's Inspect Element) will pass down pointerup events. --- src/components/swipe_click/swipe_click.js | 3 +-- src/services/gesture_service/gesture_service.js | 26 ++++++++++++++++++++++--- 2 files changed, 24 insertions(+), 5 deletions(-) (limited to 'src/services') diff --git a/src/components/swipe_click/swipe_click.js b/src/components/swipe_click/swipe_click.js index ac77154a..b979f42a 100644 --- a/src/components/swipe_click/swipe_click.js +++ b/src/components/swipe_click/swipe_click.js @@ -53,8 +53,7 @@ const SwipeClick = { this.$gesture.cancel(event) }, handleNativeClick (event) { - event.stopPropagation() - event.preventDefault() + this.$gesture.click(event) }, preview (offsets) { this.$emit('preview-requested', offsets) diff --git a/src/services/gesture_service/gesture_service.js b/src/services/gesture_service/gesture_service.js index 238b7875..cd9e3ba2 100644 --- a/src/services/gesture_service/gesture_service.js +++ b/src/services/gesture_service/gesture_service.js @@ -4,6 +4,8 @@ const DIRECTION_RIGHT = [1, 0] const DIRECTION_UP = [0, -1] const DIRECTION_DOWN = [0, 1] +const BUTTON_LEFT = 0 + const deltaCoord = (oldCoord, newCoord) => [newCoord[0] - oldCoord[0], newCoord[1] - oldCoord[1]] const touchCoord = touch => [touch.screenX, touch.screenY] @@ -23,8 +25,8 @@ const project = (v1, v2) => { return [scalar * v2[0], scalar * v2[1]] } -// const debug = console.log -const debug = () => {} +const debug = console.log +// const debug = () => {} // direction: either use the constants above or an arbitrary 2d vector. // threshold: how many Px to move from touch origin before checking if the @@ -100,11 +102,17 @@ class SwipeAndClickGesture { this._pointerId = -1 this._swiping = false this._swiped = false + this._preventNextClick = false } start (event) { debug('start() called', event) + // Only handle left click + if (event.button !== BUTTON_LEFT) { + return + } + this._startPos = pointerEventCoord(event) this._pointerId = event.pointerId debug('start pos:', this._startPos) @@ -124,6 +132,7 @@ class SwipeAndClickGesture { } cancel (event) { + debug('cancel called') if (!this._swiping || this._pointerId !== event.pointerId) { return } @@ -146,6 +155,8 @@ class SwipeAndClickGesture { debug('end: is swipe event') + debug('button = ', event.button) + // movement too small const coord = pointerEventCoord(event) const delta = deltaCoord(this._startPos, coord) @@ -171,9 +182,18 @@ class SwipeAndClickGesture { return isPositive ? 1 : -1 })() + const swiped = this._swiped if (this._swiped) { this.swipeEndCallback(sign) - } else { + } + this._reset() + if (swiped) { + this._preventNextClick = true + } + } + + click (event) { + if (!this._preventNextClick) { this.swipelessClickCallback() } this._reset() -- cgit v1.2.3-70-g09d2 From 6980e4ddf1aa8dfd8c3bba0ea6cc7de90f531ba9 Mon Sep 17 00:00:00 2001 From: Tusooa Zhu Date: Mon, 2 Aug 2021 21:19:04 -0400 Subject: Scale swipe threshold with viewport width --- src/components/media_modal/media_modal.js | 7 +++++-- src/components/swipe_click/swipe_click.js | 4 ++-- src/services/gesture_service/gesture_service.js | 11 +++++++---- 3 files changed, 14 insertions(+), 8 deletions(-) (limited to 'src/services') diff --git a/src/components/media_modal/media_modal.js b/src/components/media_modal/media_modal.js index 91413563..6a368508 100644 --- a/src/components/media_modal/media_modal.js +++ b/src/components/media_modal/media_modal.js @@ -33,7 +33,10 @@ const MediaModal = { return { loading: false, swipeDirection: GestureService.DIRECTION_LEFT, - swipeThreshold: 50, + swipeThreshold: () => { + const considerableMoveRatio = 1 / 4 + return window.innerWidth * considerableMoveRatio + }, pinchZoomMinScale: 1, pinchZoomScaleResetLimit: 1.2 } @@ -104,7 +107,7 @@ const MediaModal = { this.$refs.pinchZoom.setTransform({ scale: 1, x: 0, y: 0 }) if (sign > 0) { this.goNext() - } else { + } else if (sign < 0) { this.goPrev() } }, diff --git a/src/components/swipe_click/swipe_click.js b/src/components/swipe_click/swipe_click.js index b979f42a..238e6df8 100644 --- a/src/components/swipe_click/swipe_click.js +++ b/src/components/swipe_click/swipe_click.js @@ -31,8 +31,8 @@ const SwipeClick = { type: Array }, threshold: { - type: Number, - default: 30 + type: Function, + default: () => 30 }, perpendicularTolerance: { type: Number, diff --git a/src/services/gesture_service/gesture_service.js b/src/services/gesture_service/gesture_service.js index cd9e3ba2..97a26ba7 100644 --- a/src/services/gesture_service/gesture_service.js +++ b/src/services/gesture_service/gesture_service.js @@ -25,8 +25,8 @@ const project = (v1, v2) => { return [scalar * v2[0], scalar * v2[1]] } -const debug = console.log -// const debug = () => {} +// const debug = console.log +const debug = () => {} // direction: either use the constants above or an arbitrary 2d vector. // threshold: how many Px to move from touch origin before checking if the @@ -92,7 +92,7 @@ class SwipeAndClickGesture { this.swipeEndCallback = swipeEndCallback || nop this.swipeCancelCallback = swipeCancelCallback || nop this.swipelessClickCallback = swipelessClickCallback || nop - this.threshold = threshold + this.threshold = typeof threshold === 'function' ? threshold : () => threshold this.perpendicularTolerance = perpendicularTolerance this._reset() } @@ -162,7 +162,10 @@ class SwipeAndClickGesture { const delta = deltaCoord(this._startPos, coord) const sign = (() => { - if (vectorLength(delta) < this.threshold) { + debug( + 'threshold = ', this.threshold(), + 'vector len =', vectorLength(delta)) + if (vectorLength(delta) < this.threshold()) { return 0 } // movement is opposite from direction -- cgit v1.2.3-70-g09d2 From a485ebc2bb120e7cc6eafb7d86a5f876608f7fe1 Mon Sep 17 00:00:00 2001 From: Tusooa Zhu Date: Mon, 2 Aug 2021 22:08:04 -0400 Subject: Check whether we swiped only for mouse pointer --- src/services/gesture_service/gesture_service.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) (limited to 'src/services') diff --git a/src/services/gesture_service/gesture_service.js b/src/services/gesture_service/gesture_service.js index 97a26ba7..741d4a12 100644 --- a/src/services/gesture_service/gesture_service.js +++ b/src/services/gesture_service/gesture_service.js @@ -190,7 +190,11 @@ class SwipeAndClickGesture { this.swipeEndCallback(sign) } this._reset() - if (swiped) { + // Only a mouse will fire click event when + // the end point is far from the starting point + // so for other kinds of pointers do not check + // whether we have swiped + if (swiped && event.pointerType === 'mouse') { this._preventNextClick = true } } -- cgit v1.2.3-70-g09d2 From 5829cd98af60b758ee3a39c35f76f4da95a630c3 Mon Sep 17 00:00:00 2001 From: Tusooa Zhu Date: Wed, 8 Sep 2021 21:26:59 -0400 Subject: Clean up debug code for image pinch zoom --- src/components/media_modal/media_modal.js | 1 - src/services/gesture_service/gesture_service.js | 18 +----------------- 2 files changed, 1 insertion(+), 18 deletions(-) (limited to 'src/services') diff --git a/src/components/media_modal/media_modal.js b/src/components/media_modal/media_modal.js index 6a368508..4ed0b66f 100644 --- a/src/components/media_modal/media_modal.js +++ b/src/components/media_modal/media_modal.js @@ -103,7 +103,6 @@ const MediaModal = { this.$refs.pinchZoom.setTransform({ scale: 1, x: offsets[0], y: 0 }) }, handleSwipeEnd (sign) { - console.log('handleSwipeEnd:', sign) this.$refs.pinchZoom.setTransform({ scale: 1, x: 0, y: 0 }) if (sign > 0) { this.goNext() diff --git a/src/services/gesture_service/gesture_service.js b/src/services/gesture_service/gesture_service.js index 741d4a12..94da3f43 100644 --- a/src/services/gesture_service/gesture_service.js +++ b/src/services/gesture_service/gesture_service.js @@ -25,9 +25,6 @@ const project = (v1, v2) => { return [scalar * v2[0], scalar * v2[1]] } -// const debug = console.log -const debug = () => {} - // direction: either use the constants above or an arbitrary 2d vector. // threshold: how many Px to move from touch origin before checking if the // callback should be called. @@ -86,7 +83,7 @@ class SwipeAndClickGesture { swipelessClickCallback, threshold = 30, perpendicularTolerance = 1.0 }) { - const nop = () => { debug('Warning: Not implemented') } + const nop = () => {} this.direction = direction this.swipePreviewCallback = swipePreviewCallback || nop this.swipeEndCallback = swipeEndCallback || nop @@ -106,8 +103,6 @@ class SwipeAndClickGesture { } start (event) { - debug('start() called', event) - // Only handle left click if (event.button !== BUTTON_LEFT) { return @@ -115,7 +110,6 @@ class SwipeAndClickGesture { this._startPos = pointerEventCoord(event) this._pointerId = event.pointerId - debug('start pos:', this._startPos) this._swiping = true this._swiped = false } @@ -132,7 +126,6 @@ class SwipeAndClickGesture { } cancel (event) { - debug('cancel called') if (!this._swiping || this._pointerId !== event.pointerId) { return } @@ -142,29 +135,20 @@ class SwipeAndClickGesture { end (event) { if (!this._swiping) { - debug('not swiping') return } if (this._pointerId !== event.pointerId) { - debug('pointer id does not match') return } this._swiping = false - debug('end: is swipe event') - - debug('button = ', event.button) - // movement too small const coord = pointerEventCoord(event) const delta = deltaCoord(this._startPos, coord) const sign = (() => { - debug( - 'threshold = ', this.threshold(), - 'vector len =', vectorLength(delta)) if (vectorLength(delta) < this.threshold()) { return 0 } -- cgit v1.2.3-70-g09d2 From 7dd1a0dd30773fd51508feaf93be53bdb744ec79 Mon Sep 17 00:00:00 2001 From: Tusooa Zhu Date: Sun, 20 Feb 2022 22:45:58 -0500 Subject: Prevent hiding media viewer if swiped over SwipeClick --- src/components/media_modal/media_modal.js | 9 +++++++++ src/components/media_modal/media_modal.vue | 3 ++- src/services/gesture_service/gesture_service.js | 8 +++++--- 3 files changed, 16 insertions(+), 4 deletions(-) (limited to 'src/services') diff --git a/src/components/media_modal/media_modal.js b/src/components/media_modal/media_modal.js index 4ed0b66f..0c19ed50 100644 --- a/src/components/media_modal/media_modal.js +++ b/src/components/media_modal/media_modal.js @@ -76,6 +76,15 @@ const MediaModal = { this.$store.dispatch('closeMediaViewer') }, transitionTime) }, + hideIfNotSwiped (event) { + // If we have swiped over SwipeClick, do not trigger hide + const comp = this.$refs.swipeClick + if (!comp) { + this.hide() + } else { + comp.$gesture.click(event) + } + }, goPrev () { if (this.canNavigate) { const prevIndex = this.currentIndex === 0 ? this.media.length - 1 : (this.currentIndex - 1) diff --git a/src/components/media_modal/media_modal.vue b/src/components/media_modal/media_modal.vue index ff9c4953..1aa66a55 100644 --- a/src/components/media_modal/media_modal.vue +++ b/src/components/media_modal/media_modal.vue @@ -2,10 +2,11 @@ {} this.direction = direction @@ -90,6 +92,7 @@ class SwipeAndClickGesture { this.swipeCancelCallback = swipeCancelCallback || nop this.swipelessClickCallback = swipelessClickCallback || nop this.threshold = typeof threshold === 'function' ? threshold : () => threshold + this.disableClickThreshold = typeof disableClickThreshold === 'function' ? disableClickThreshold : () => disableClickThreshold this.perpendicularTolerance = perpendicularTolerance this._reset() } @@ -169,7 +172,6 @@ class SwipeAndClickGesture { return isPositive ? 1 : -1 })() - const swiped = this._swiped if (this._swiped) { this.swipeEndCallback(sign) } @@ -178,7 +180,7 @@ class SwipeAndClickGesture { // the end point is far from the starting point // so for other kinds of pointers do not check // whether we have swiped - if (swiped && event.pointerType === 'mouse') { + if (vectorLength(delta) >= this.disableClickThreshold() && event.pointerType === 'mouse') { this._preventNextClick = true } } -- cgit v1.2.3-70-g09d2