diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 9d42288e..7f6d3c92 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -24,7 +24,7 @@ test:
- apt install firefox-esr -y --no-install-recommends
- firefox --version
- yarn
- - npm run unit
+ - yarn unit
build:
stage: build
diff --git a/src/components/settings/settings.vue b/src/components/settings/settings.vue
index a83489d2..c4021137 100644
--- a/src/components/settings/settings.vue
+++ b/src/components/settings/settings.vue
@@ -270,6 +270,17 @@
+
+
diff --git a/src/components/status/status.js b/src/components/status/status.js
index 4fbd5ac3..714ea6d2 100644
--- a/src/components/status/status.js
+++ b/src/components/status/status.js
@@ -13,10 +13,11 @@ import Timeago from '../timeago/timeago.vue'
import StatusPopover from '../status_popover/status_popover.vue'
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
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 { mentionMatchesUrl, extractTagFromUrl } from 'src/services/matcher/matcher.service.js'
import { filter, unescape, uniqBy } from 'lodash'
-import { mapGetters } from 'vuex'
+import { mapGetters, mapState } from 'vuex'
const Status = {
name: 'Status',
@@ -42,8 +43,8 @@ const Status = {
showingTall: this.inConversation && this.focused,
showingLongSubject: false,
error: null,
- expandingSubject: !this.$store.getters.mergedConfig.collapseMessageWithSubject,
- betterShadow: this.$store.state.interface.browserSupport.cssFilter
+ // not as computed because it sets the initial state which will be changed later
+ expandingSubject: !this.$store.getters.mergedConfig.collapseMessageWithSubject
}
},
computed: {
@@ -103,7 +104,7 @@ const Status = {
return this.$store.state.statuses.allStatusesObject[this.status.id]
},
loggedIn () {
- return !!this.$store.state.users.currentUser
+ return !!this.currentUser
},
muteWordHits () {
const statusText = this.status.text.toLowerCase()
@@ -163,7 +164,7 @@ const Status = {
if (this.inConversation || !this.isReply) {
return false
}
- if (this.status.user.id === this.$store.state.users.currentUser.id) {
+ if (this.status.user.id === this.currentUser.id) {
return false
}
if (this.status.type === 'retweet') {
@@ -178,7 +179,7 @@ const Status = {
if (checkFollowing && taggedUser && taggedUser.following) {
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
}
}
@@ -255,11 +256,41 @@ const Status = {
maxThumbnails () {
return this.mergedConfig.maxThumbnails
},
+ postBodyHtml () {
+ const html = this.status.statusnet_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
+ }
+ },
contentHtml () {
if (!this.status.summary_html) {
- return this.status.statusnet_html
+ return this.postBodyHtml
}
- return this.status.summary_html + '
' + this.status.statusnet_html
+ return this.status.summary_html + '
' + this.postBodyHtml
},
combinedFavsAndRepeatsUsers () {
// 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')
},
ownStatus () {
- return this.status.user.id === this.$store.state.users.currentUser.id
+ return this.status.user.id === this.currentUser.id
},
tags () {
return this.status.tags.filter(tagObj => tagObj.hasOwnProperty('name')).map(tagObj => tagObj.name).join(' ')
@@ -278,7 +309,11 @@ const Status = {
hidePostStats () {
return this.mergedConfig.hidePostStats
},
- ...mapGetters(['mergedConfig'])
+ ...mapGetters(['mergedConfig']),
+ ...mapState({
+ betterShadow: state => state.interface.browserSupport.cssFilter,
+ currentUser: state => state.users.currentUser
+ })
},
components: {
Attachment,
diff --git a/src/components/status/status.vue b/src/components/status/status.vue
index 65778b2e..d291e762 100644
--- a/src/components/status/status.vue
+++ b/src/components/status/status.vue
@@ -606,7 +606,7 @@ $status-margin: 0.75em;
height: 100%;
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
+ /* Autoprefixed seem to ignore this one, and also syntax is different */
-webkit-mask-composite: xor;
mask-composite: exclude;
}
@@ -752,7 +752,8 @@ $status-margin: 0.75em;
}
.greentext {
- color: green;
+ color: $fallback--cGreen;
+ color: var(--cGreen, $fallback--cGreen);
}
.status-conversation {
diff --git a/src/i18n/en.json b/src/i18n/en.json
index ad3e671d..85146ef5 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -370,6 +370,8 @@
"false": "no",
"true": "yes"
},
+ "fun": "Fun",
+ "greentext": "Meme arrows",
"notifications": "Notifications",
"notification_setting": "Receive notifications from:",
"notification_setting_follows": "Users you follow",
diff --git a/src/i18n/ru.json b/src/i18n/ru.json
index f8bcd996..19e10f1e 100644
--- a/src/i18n/ru.json
+++ b/src/i18n/ru.json
@@ -174,6 +174,8 @@
"name_bio": "Имя и описание",
"new_email": "Новый email",
"new_password": "Новый пароль",
+ "fun": "Потешное",
+ "greentext": "Мемные стрелочки",
"notification_visibility": "Показывать уведомления",
"notification_visibility_follows": "Подписки",
"notification_visibility_likes": "Лайки",
diff --git a/src/modules/config.js b/src/modules/config.js
index d4819ee8..329b4091 100644
--- a/src/modules/config.js
+++ b/src/modules/config.js
@@ -45,6 +45,7 @@ export const defaultState = {
playVideosInModal: false,
useOneClickNsfw: false,
useContainFit: false,
+ greentext: undefined, // instance default
hidePostStats: undefined, // instance default
hideUserStats: undefined // instance default
}
diff --git a/src/modules/instance.js b/src/modules/instance.js
index 7b0e0da4..96f14ed5 100644
--- a/src/modules/instance.js
+++ b/src/modules/instance.js
@@ -32,6 +32,7 @@ const defaultState = {
noAttachmentLinks: false,
showFeaturesPanel: true,
minimalScopesMode: false,
+ greentext: false,
// Nasty stuff
pleromaBackend: true,
diff --git a/src/services/tiny_post_html_processor/tiny_post_html_processor.service.js b/src/services/tiny_post_html_processor/tiny_post_html_processor.service.js
new file mode 100644
index 00000000..de6f20ef
--- /dev/null
+++ b/src/services/tiny_post_html_processor/tiny_post_html_processor.service.js
@@ -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
+ 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
+}
diff --git a/test/unit/specs/services/tiny_post_html_processor/tiny_post_html_processor.spec.js b/test/unit/specs/services/tiny_post_html_processor/tiny_post_html_processor.spec.js
new file mode 100644
index 00000000..f301429d
--- /dev/null
+++ b/test/unit/specs/services/tiny_post_html_processor/tiny_post_html_processor.spec.js
@@ -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
23 4
5 \n 6 7
8
\n
'
+ expect(processHtml(inputOutput, processorKeep)).to.eql(inputOutput)
+ })
+
+ it('fed with possibly broken HTML with invalid tags/composition', () => {
+ const inputOutput = ' ayylmao '
+ expect(processHtml(inputOutput, processorKeep)).to.eql(inputOutput)
+ })
+
+ it('fed with very broken HTML with broken composition', () => {
+ const inputOutput = ' lmao what whats going on