Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Collapse quoted text in HTML message #6770

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
143 changes: 143 additions & 0 deletions src/components/HtmlBlockQuote.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
<template>
<div class="quotes">
<a class="btn" @click="showAllQuotes()">
<CommentQuote :size="18" />
Toggle {{ total }} quotes
</a>
<HtmlBlockQuoteItem :quote="quotes" @toggleQuote="toggleQuote(quotes)" />
</div>
</template>

<script>

import HtmlBlockQuoteItem from './HtmlBlockQuoteItem'
import CommentQuote from 'vue-material-design-icons/CommentQuote'

const REGEXP_EMAIL = /([!#-'*+/-9=?A-Z^-~-]+(\.[!#-'*+/-9=?A-Z^-~-]+)*|(\[\]!#-[^-~ \t]|(\\[\t -~]))+")@([!#-'*+/-9=?A-Z^-~-]+(\.[!#-'*+/-9=?A-Z^-~-]+)*|\[[\t -Z^-~]*])/
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is the part I'd like to see abstracted and tested 😆

const REGEXP_LABEL = /"(.*)"/

export default {
name: 'HtmlBlockQuote',
components: {
HtmlBlockQuoteItem,
CommentQuote,
},
props: {
quote: {
type: HTMLElement,
required: true,
},
blockquote: {
type: HTMLElement,
required: true,
},
},
data() {
return {
quotes: {},
total: null,
}
},
mounted() {
this.quotes = this.findAndToggleBlockquotesRecursive(this.quote, this.blockquote)
},
methods: {
findAndToggleBlockquotesRecursive(quote, blockquote) {
if (quote === null && blockquote !== null) {
quote = blockquote
}

if (quote !== null && blockquote !== null) {
const _quote = this.getQuoteHeaderData(quote)

if (blockquote.nodeName === 'BLOCKQUOTE') {
const subBlockquote = blockquote.querySelector('blockquote')
const subQuote = subBlockquote !== null ? subBlockquote.previousElementSibling : null
if (subBlockquote !== null) {
subBlockquote.remove()
if (subQuote !== null) {
subQuote.remove()
}
}

this.total++

return {
id: this.quotes.length,
text: _quote.text,
email: _quote.email,
label: _quote.label,
date: _quote.date,
hide: true,
blockquoteText: blockquote.innerHTML,
childs: this.findAndToggleBlockquotesRecursive(subQuote, subBlockquote),
}
}
}
},

getQuoteHeaderData(quote) {
const text = quote.innerText.trim()
const email = REGEXP_EMAIL.exec(text)
const label = REGEXP_LABEL.exec(text)
const date = text.split(' – ')

const data = {
text: null,
label: label !== null ? label[1] : text,
email: email !== null ? email[0] : null,
date: date !== null && date.length > 1 ? date[1].trim() : null,
}
data.text = data.email === null && data.label === null ? null : text
return data
},

showAllQuotes(quote) {
const hide = this.quotes.hide
const _quote = !quote ? this.quotes : quote
if (typeof (_quote.childs) !== 'undefined') {
this.showAllQuotes(_quote.childs)
}
_quote.hide = !hide
},

toggleQuote(quote) {
quote.hide = !quote.hide
},
},
}
</script>

<style lang="scss" scoped>

a.btn {
display:inline-flex;
margin: 20px 0 20px -26px;
color:#666;
text-decoration: none;
cursor: pointer;

& > span {
margin-right: 5px;
}
}

.show-all-quotes {
display:inline-flex;
align-items: center;
margin-bottom: 5px;
padding: 4px;
background: #ddd;
border-radius: 20px;
cursor: pointer;
}

.quotes {
margin-top: 16px;
margin-left: 50px;

.quote {
margin-left: 0;
}
}
</style>
135 changes: 135 additions & 0 deletions src/components/HtmlBlockQuoteItem.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
<template>
<div class="quote" :class="{ 'last': !quote.childs }">
<div class="quote-header" :class="{ 'active': !quote.hide }" @click="$emit('toggleQuote')">

Check failure on line 3 in src/components/HtmlBlockQuoteItem.vue

View workflow job for this annotation

GitHub Actions / eslint

Custom event name 'toggleQuote' must be kebab-case
<Avatar v-if="quote.label"
:url="avatarUrl"
:email="quote.email"
:display-name="quote.label"
:disable-tooltip="true"
:disable-menu="true"
:size="24" />
<FormatQuoteClose v-else-if="quote.hide" :size="24" class="quoterdiv" />
<FormatQuoteOpen v-else :size="24" class="quoterdiv" />
<div v-if="!quote.label">
{{ quote.text }}
</div>
<div v-else class="quote-header-text">
<span>{{ quote.label }}
<a :href="`mailto:${quote.email}`">{{ quote.email }}</a>
<span v-if="quote.date" class="quote-date">{{ quote.date }}</span>
</span>
</div>
</div>
<div v-if="!quote.hide" class="quote-body">
<div class="quote-content" v-html="quote.blockquoteText" />

Check warning on line 24 in src/components/HtmlBlockQuoteItem.vue

View workflow job for this annotation

GitHub Actions / eslint

'v-html' directive can lead to XSS attack
<HtmlBlockQuoteItem v-if="quote.childs" :quote="quote.childs" @toggleQuote="toggleQuote(quote.childs)" />
</div>
</div>
</template>

<script>
// import { fetchAvatarUrlMemoized } from '../service/AvatarService'
import Avatar from '@nextcloud/vue/dist/Components/Avatar'

Check failure on line 32 in src/components/HtmlBlockQuoteItem.vue

View workflow job for this annotation

GitHub Actions / eslint

Unable to resolve path to module '@nextcloud/vue/dist/Components/Avatar'

Check failure on line 32 in src/components/HtmlBlockQuoteItem.vue

View workflow job for this annotation

GitHub Actions / eslint

"@nextcloud/vue/dist/Components/Avatar" is not found
import FormatQuoteClose from 'vue-material-design-icons/FormatQuoteClose'
import FormatQuoteOpen from 'vue-material-design-icons/FormatQuoteOpen'

export default {
name: 'HtmlBlockQuoteItem',
components: {
Avatar,
FormatQuoteClose,
FormatQuoteOpen,
},
props: {
quote: {
type: Object,
required: true,
},
},
data() {
return {
hide: this.quote.hide,
avatarUrl: undefined,
}
},
async mounted() {
if (this.quote.email !== '') {
// this.avatarUrl = await fetchAvatarUrlMemoized(this.quote.email)
}
},
methods: {
toggleQuote(quote) {
quote.hide = !quote.hide
},
},
}
</script>

<style lang="scss" scoped>
.quote {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

margin-bottom: 10px;

.quote-header {
display: inline-flex;
flex-wrap: wrap;
align-items: center;
cursor: pointer;
transition: 0.3s;
padding: 3px 16px 3px 3px;
margin-left: -34px;

.avatardiv {
border-radius: 50%;
color: #fff;
margin-right: 6px;
width: 24px;
}

.quoterdiv {
border-radius: 50%;
color: #333;
margin-right: 6px;
}

.quote-header-text {
width: calc(100% - 30px)
}

&:hover, &.active {
background: #efefef;
border-radius: 20px;
}

.quote-date {
opacity: 0.5;
}
}

.quote-body {
padding: 10px 0;

.quote-content {
position: relative;
color: #666;
display: inline-block;

&:before {
content: "";
position: absolute;
width: 24px;
top: -13px;
bottom: -13px;
left: -20px;
border-left: 2px solid #efefef;
z-index: -1;
}
}
}
&.last {
.quote-content:before {
display: none
}
}
}

</style>
32 changes: 32 additions & 0 deletions src/html-body-quotes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import Vue from 'vue'
import Nextcloud from './mixins/Nextcloud'
import HtmlBlockQuote from './components/HtmlBlockQuote'

const vueOfBlockQuote = () => {
// very hardcode logic
const blockquote = document.querySelector('blockquote')
if (blockquote !== null) {
const quote = blockquote.previousElementSibling
// remove first quote in body, if exists
quote.remove()

if (quote === null) {
return false
}
Vue.mixin(Nextcloud)
return new Vue({
el: document.querySelector('blockquote'),
render: (h) => h(HtmlBlockQuote, {
props: {
quote,
blockquote,
},
}),
})
}
return false
}

export {
vueOfBlockQuote,
}
4 changes: 4 additions & 0 deletions src/html-response.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,15 @@ import '../css/html-response.css'
// iframe-resizer client script
import 'iframe-resizer/js/iframeResizer.contentWindow.js'

import { vueOfBlockQuote } from './html-body-quotes.js'

// Fix width of some newsletter mails
document.addEventListener('DOMContentLoaded', function() {
for (const el of document.querySelectorAll('*')) {
if (!el.style['max-width']) {
el.style['max-width'] = '100%'
}
}

vueOfBlockQuote()
})
Loading