Merge branch 'greentext-strikes-back' into 'develop'

⑨ Added greentext support ⑨

Closes #9

See merge request pleroma/pleroma-fe!994
This commit is contained in:
HJ 2019-11-19 14:22:17 +00:00
commit 0eda60eeb4
10 changed files with 256 additions and 13 deletions

View file

@ -24,7 +24,7 @@ test:
- apt install firefox-esr -y --no-install-recommends - apt install firefox-esr -y --no-install-recommends
- firefox --version - firefox --version
- yarn - yarn
- npm run unit - yarn unit
build: build:
stage: build stage: build

View file

@ -270,6 +270,17 @@
</li> </li>
</ul> </ul>
</div> </div>
<div class="setting-item">
<h2>{{ $t('settings.fun') }}</h2>
<ul class="setting-list">
<li>
<Checkbox v-model="greentext">
{{ $t('settings.greentext') }} {{ $t('settings.instance_default', { value: greentextLocalizedValue }) }}
</Checkbox>
</li>
</ul>
</div>
</div> </div>
<div :label="$t('settings.theme')"> <div :label="$t('settings.theme')">

View file

@ -13,10 +13,11 @@ import Timeago from '../timeago/timeago.vue'
import StatusPopover from '../status_popover/status_popover.vue' import StatusPopover from '../status_popover/status_popover.vue'
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
import fileType from 'src/services/file_type/file_type.service' import fileType from 'src/services/file_type/file_type.service'
import { processHtml } from 'src/services/tiny_post_html_processor/tiny_post_html_processor.service.js'
import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js' import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
import { mentionMatchesUrl, extractTagFromUrl } from 'src/services/matcher/matcher.service.js' import { mentionMatchesUrl, extractTagFromUrl } from 'src/services/matcher/matcher.service.js'
import { filter, unescape, uniqBy } from 'lodash' import { filter, unescape, uniqBy } from 'lodash'
import { mapGetters } from 'vuex' import { mapGetters, mapState } from 'vuex'
const Status = { const Status = {
name: 'Status', name: 'Status',
@ -42,8 +43,8 @@ const Status = {
showingTall: this.inConversation && this.focused, showingTall: this.inConversation && this.focused,
showingLongSubject: false, showingLongSubject: false,
error: null, error: null,
expandingSubject: !this.$store.getters.mergedConfig.collapseMessageWithSubject, // not as computed because it sets the initial state which will be changed later
betterShadow: this.$store.state.interface.browserSupport.cssFilter expandingSubject: !this.$store.getters.mergedConfig.collapseMessageWithSubject
} }
}, },
computed: { computed: {
@ -103,7 +104,7 @@ const Status = {
return this.$store.state.statuses.allStatusesObject[this.status.id] return this.$store.state.statuses.allStatusesObject[this.status.id]
}, },
loggedIn () { loggedIn () {
return !!this.$store.state.users.currentUser return !!this.currentUser
}, },
muteWordHits () { muteWordHits () {
const statusText = this.status.text.toLowerCase() const statusText = this.status.text.toLowerCase()
@ -163,7 +164,7 @@ const Status = {
if (this.inConversation || !this.isReply) { if (this.inConversation || !this.isReply) {
return false return false
} }
if (this.status.user.id === this.$store.state.users.currentUser.id) { if (this.status.user.id === this.currentUser.id) {
return false return false
} }
if (this.status.type === 'retweet') { if (this.status.type === 'retweet') {
@ -178,7 +179,7 @@ const Status = {
if (checkFollowing && taggedUser && taggedUser.following) { if (checkFollowing && taggedUser && taggedUser.following) {
return false return false
} }
if (this.status.attentions[i].id === this.$store.state.users.currentUser.id) { if (this.status.attentions[i].id === this.currentUser.id) {
return false return false
} }
} }
@ -255,11 +256,41 @@ const Status = {
maxThumbnails () { maxThumbnails () {
return this.mergedConfig.maxThumbnails return this.mergedConfig.maxThumbnails
}, },
postBodyHtml () {
const html = this.status.statusnet_html
if (this.mergedConfig.greentext) {
try {
if (html.includes('&gt;')) {
// This checks if post has '>' at the beginning, excluding mentions so that @mention >impying works
return processHtml(html, (string) => {
if (string.includes('&gt;') &&
string
.replace(/<[^>]+?>/gi, '') // remove all tags
.replace(/@\w+/gi, '') // remove mentions (even failed ones)
.trim()
.startsWith('&gt;')) {
return `<span class='greentext'>${string}</span>`
} else {
return string
}
})
} else {
return html
}
} catch (e) {
console.err('Failed to process status html', e)
return html
}
} else {
return html
}
},
contentHtml () { contentHtml () {
if (!this.status.summary_html) { if (!this.status.summary_html) {
return this.status.statusnet_html return this.postBodyHtml
} }
return this.status.summary_html + '<br />' + this.status.statusnet_html return this.status.summary_html + '<br />' + this.postBodyHtml
}, },
combinedFavsAndRepeatsUsers () { combinedFavsAndRepeatsUsers () {
// Use the status from the global status repository since favs and repeats are saved in it // Use the status from the global status repository since favs and repeats are saved in it
@ -270,7 +301,7 @@ const Status = {
return uniqBy(combinedUsers, 'id') return uniqBy(combinedUsers, 'id')
}, },
ownStatus () { ownStatus () {
return this.status.user.id === this.$store.state.users.currentUser.id return this.status.user.id === this.currentUser.id
}, },
tags () { tags () {
return this.status.tags.filter(tagObj => tagObj.hasOwnProperty('name')).map(tagObj => tagObj.name).join(' ') return this.status.tags.filter(tagObj => tagObj.hasOwnProperty('name')).map(tagObj => tagObj.name).join(' ')
@ -278,7 +309,11 @@ const Status = {
hidePostStats () { hidePostStats () {
return this.mergedConfig.hidePostStats return this.mergedConfig.hidePostStats
}, },
...mapGetters(['mergedConfig']) ...mapGetters(['mergedConfig']),
...mapState({
betterShadow: state => state.interface.browserSupport.cssFilter,
currentUser: state => state.users.currentUser
})
}, },
components: { components: {
Attachment, Attachment,

View file

@ -606,7 +606,7 @@ $status-margin: 0.75em;
height: 100%; height: 100%;
mask: linear-gradient(to top, white, transparent) bottom/100% 70px no-repeat, mask: linear-gradient(to top, white, transparent) bottom/100% 70px no-repeat,
linear-gradient(to top, white, white); linear-gradient(to top, white, white);
// Autoprefixed seem to ignore this one, and also syntax is different /* Autoprefixed seem to ignore this one, and also syntax is different */
-webkit-mask-composite: xor; -webkit-mask-composite: xor;
mask-composite: exclude; mask-composite: exclude;
} }
@ -752,7 +752,8 @@ $status-margin: 0.75em;
} }
.greentext { .greentext {
color: green; color: $fallback--cGreen;
color: var(--cGreen, $fallback--cGreen);
} }
.status-conversation { .status-conversation {

View file

@ -370,6 +370,8 @@
"false": "no", "false": "no",
"true": "yes" "true": "yes"
}, },
"fun": "Fun",
"greentext": "Meme arrows",
"notifications": "Notifications", "notifications": "Notifications",
"notification_setting": "Receive notifications from:", "notification_setting": "Receive notifications from:",
"notification_setting_follows": "Users you follow", "notification_setting_follows": "Users you follow",

View file

@ -174,6 +174,8 @@
"name_bio": "Имя и описание", "name_bio": "Имя и описание",
"new_email": "Новый email", "new_email": "Новый email",
"new_password": "Новый пароль", "new_password": "Новый пароль",
"fun": "Потешное",
"greentext": "Мемные стрелочки",
"notification_visibility": "Показывать уведомления", "notification_visibility": "Показывать уведомления",
"notification_visibility_follows": "Подписки", "notification_visibility_follows": "Подписки",
"notification_visibility_likes": "Лайки", "notification_visibility_likes": "Лайки",

View file

@ -45,6 +45,7 @@ export const defaultState = {
playVideosInModal: false, playVideosInModal: false,
useOneClickNsfw: false, useOneClickNsfw: false,
useContainFit: false, useContainFit: false,
greentext: undefined, // instance default
hidePostStats: undefined, // instance default hidePostStats: undefined, // instance default
hideUserStats: undefined // instance default hideUserStats: undefined // instance default
} }

View file

@ -32,6 +32,7 @@ const defaultState = {
noAttachmentLinks: false, noAttachmentLinks: false,
showFeaturesPanel: true, showFeaturesPanel: true,
minimalScopesMode: false, minimalScopesMode: false,
greentext: false,
// Nasty stuff // Nasty stuff
pleromaBackend: true, pleromaBackend: true,

View file

@ -0,0 +1,94 @@
/**
* This is a tiny purpose-built HTML parser/processor. This basically detects any type of visual newline and
* allows it to be processed, useful for greentexting, mostly
*
* known issue: doesn't handle CDATA so nested CDATA might not work well
*
* @param {Object} input - input data
* @param {(string) => string} processor - function that will be called on every line
* @return {string} processed html
*/
export const processHtml = (html, processor) => {
const handledTags = new Set(['p', 'br', 'div'])
const openCloseTags = new Set(['p', 'div'])
let buffer = '' // Current output buffer
const level = [] // 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
// Extracts tag name from tag, i.e. <span a="b"> => span
const getTagName = (tag) => {
const result = /(?:<\/(\w+)>|<(\w+)\s?[^/]*?\/?>)/gi.exec(tag)
return result && (result[1] || result[2])
}
const flush = () => { // Processes current line buffer, adds it to output buffer and clears line buffer
if (textBuffer.trim().length > 0) {
buffer += processor(textBuffer)
} else {
buffer += textBuffer
}
textBuffer = ''
}
const handleBr = (tag) => { // handles single newlines/linebreaks/selfclosing
flush()
buffer += tag
}
const handleOpen = (tag) => { // handles opening tags
flush()
buffer += tag
level.push(tag)
}
const handleClose = (tag) => { // handles closing tags
flush()
buffer += tag
if (level[level.length - 1] === tag) {
level.pop()
}
}
for (let i = 0; i < html.length; i++) {
const char = html[i]
if (char === '<' && tagBuffer === null) {
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 (handledTags.has(tagName)) {
if (tagName === 'br') {
handleBr(tagFull)
} else if (openCloseTags.has(tagName)) {
if (tagFull[1] === '/') {
handleClose(tagFull)
} else if (tagFull[tagFull.length - 2] === '/') {
// self-closing
handleBr(tagFull)
} else {
handleOpen(tagFull)
}
}
} else {
textBuffer += tagFull
}
} else if (char === '\n') {
handleBr(char)
} else {
textBuffer += char
}
}
if (tagBuffer) {
textBuffer += tagBuffer
}
flush()
return buffer
}

View file

@ -0,0 +1,96 @@
import { processHtml } from 'src/services/tiny_post_html_processor/tiny_post_html_processor.service.js'
describe('TinyPostHTMLProcessor', () => {
describe('with processor that keeps original line should not make any changes to HTML when', () => {
const processorKeep = (line) => line
it('fed with regular HTML with newlines', () => {
const inputOutput = '1<br/>2<p class="lol">3 4</p> 5 \n 6 <p > 7 <br> 8 </p> <br>\n<br/>'
expect(processHtml(inputOutput, processorKeep)).to.eql(inputOutput)
})
it('fed with possibly broken HTML with invalid tags/composition', () => {
const inputOutput = '<feeee dwdwddddddw> <i>ayy<b>lm</i>ao</b> </section>'
expect(processHtml(inputOutput, processorKeep)).to.eql(inputOutput)
})
it('fed with very broken HTML with broken composition', () => {
const inputOutput = '</p> lmao what </div> whats going on <div> wha <p>'
expect(processHtml(inputOutput, processorKeep)).to.eql(inputOutput)
})
it('fed with sorta valid HTML but tags aren\'t closed', () => {
const inputOutput = 'just leaving a <div> hanging'
expect(processHtml(inputOutput, processorKeep)).to.eql(inputOutput)
})
it('fed with not really HTML at this point... tags that aren\'t finished', () => {
const inputOutput = 'do you expect me to finish this <div class='
expect(processHtml(inputOutput, processorKeep)).to.eql(inputOutput)
})
it('fed with dubiously valid HTML (p within p and also div inside p)', () => {
const inputOutput = 'look ma <p> p \nwithin <p> p! </p> and a <br/><div>div!</div></p>'
expect(processHtml(inputOutput, processorKeep)).to.eql(inputOutput)
})
it('fed with maybe valid HTML? self-closing divs and ps', () => {
const inputOutput = 'a <div class="what"/> what now <p aria-label="wtf"/> ?'
expect(processHtml(inputOutput, processorKeep)).to.eql(inputOutput)
})
it('fed with valid XHTML containing a CDATA', () => {
const inputOutput = 'Yes, it is me, <![CDATA[DIO]]>'
expect(processHtml(inputOutput, processorKeep)).to.eql(inputOutput)
})
})
describe('with processor that replaces lines with word "_" should match expected line when', () => {
const processorReplace = (line) => '_'
it('fed with regular HTML with newlines', () => {
const input = '1<br/>2<p class="lol">3 4</p> 5 \n 6 <p > 7 <br> 8 </p> <br>\n<br/>'
const output = '_<br/>_<p class="lol">_</p>_\n_<p >_<br>_</p> <br>\n<br/>'
expect(processHtml(input, processorReplace)).to.eql(output)
})
it('fed with possibly broken HTML with invalid tags/composition', () => {
const input = '<feeee dwdwddddddw> <i>ayy<b>lm</i>ao</b> </section>'
const output = '_'
expect(processHtml(input, processorReplace)).to.eql(output)
})
it('fed with very broken HTML with broken composition', () => {
const input = '</p> lmao what </div> whats going on <div> wha <p>'
const output = '</p>_</div>_<div>_<p>'
expect(processHtml(input, processorReplace)).to.eql(output)
})
it('fed with sorta valid HTML but tags aren\'t closed', () => {
const input = 'just leaving a <div> hanging'
const output = '_<div>_'
expect(processHtml(input, processorReplace)).to.eql(output)
})
it('fed with not really HTML at this point... tags that aren\'t finished', () => {
const input = 'do you expect me to finish this <div class='
const output = '_'
expect(processHtml(input, processorReplace)).to.eql(output)
})
it('fed with dubiously valid HTML (p within p and also div inside p)', () => {
const input = 'look ma <p> p \nwithin <p> p! </p> and a <br/><div>div!</div></p>'
const output = '_<p>_\n_<p>_</p>_<br/><div>_</div></p>'
expect(processHtml(input, processorReplace)).to.eql(output)
})
it('fed with maybe valid HTML? self-closing divs and ps', () => {
const input = 'a <div class="what"/> what now <p aria-label="wtf"/> ?'
const output = '_<div class="what"/>_<p aria-label="wtf"/>_'
expect(processHtml(input, processorReplace)).to.eql(output)
})
it('fed with valid XHTML containing a CDATA', () => {
const input = 'Yes, it is me, <![CDATA[DIO]]>'
const output = '_'
expect(processHtml(input, processorReplace)).to.eql(output)
})
})
})