diff --git a/component/api/pom.xml b/component/api/pom.xml index cc1963c8843..f124e2c3c83 100644 --- a/component/api/pom.xml +++ b/component/api/pom.xml @@ -22,7 +22,7 @@ social-component io.meeds.social - 7.3.x-SNAPSHOT + 7.3.x-maintenance-SNAPSHOT social-component-api Meeds:: PLF:: Social API diff --git a/component/common/pom.xml b/component/common/pom.xml index d997d1baeb1..3a76ed94f5f 100644 --- a/component/common/pom.xml +++ b/component/common/pom.xml @@ -22,7 +22,7 @@ social-component io.meeds.social - 7.3.x-SNAPSHOT + 7.3.x-maintenance-SNAPSHOT io.meeds.social social-component-common diff --git a/component/core/pom.xml b/component/core/pom.xml index 916070138fc..8f18ddf45d3 100644 --- a/component/core/pom.xml +++ b/component/core/pom.xml @@ -22,7 +22,7 @@ social-component io.meeds.social - 7.3.x-SNAPSHOT + 7.3.x-maintenance-SNAPSHOT social-component-core Meeds:: PLF:: Social Core Component diff --git a/component/notification/pom.xml b/component/notification/pom.xml index 2ca9bbaac27..35a6b4f1ea5 100644 --- a/component/notification/pom.xml +++ b/component/notification/pom.xml @@ -22,7 +22,7 @@ social-component io.meeds.social - 7.3.x-SNAPSHOT + 7.3.x-maintenance-SNAPSHOT social-component-notification Meeds:: PLF:: Social Notification Component diff --git a/component/oauth-auth/pom.xml b/component/oauth-auth/pom.xml index c7bb2a3e80f..6fc5359ee0b 100644 --- a/component/oauth-auth/pom.xml +++ b/component/oauth-auth/pom.xml @@ -21,7 +21,7 @@ social-component io.meeds.social - 7.3.x-SNAPSHOT + 7.3.x-maintenance-SNAPSHOT 4.0.0 diff --git a/component/pom.xml b/component/pom.xml index b5976751ba8..4138bbb02cb 100644 --- a/component/pom.xml +++ b/component/pom.xml @@ -22,7 +22,7 @@ social io.meeds.social - 7.3.x-SNAPSHOT + 7.3.x-maintenance-SNAPSHOT social-component pom diff --git a/component/service/pom.xml b/component/service/pom.xml index c6b52444ca4..0b8aa3c8aec 100644 --- a/component/service/pom.xml +++ b/component/service/pom.xml @@ -22,7 +22,7 @@ social-component io.meeds.social - 7.3.x-SNAPSHOT + 7.3.x-maintenance-SNAPSHOT social-component-service Meeds:: PLF:: Social Service Component diff --git a/component/web/pom.xml b/component/web/pom.xml index 90866efe91e..4e2dc359a26 100644 --- a/component/web/pom.xml +++ b/component/web/pom.xml @@ -22,7 +22,7 @@ social-component io.meeds.social - 7.3.x-SNAPSHOT + 7.3.x-maintenance-SNAPSHOT io.meeds.social social-component-web diff --git a/pom.xml b/pom.xml index ab09c7291e8..557d51871e9 100644 --- a/pom.xml +++ b/pom.xml @@ -27,7 +27,7 @@ io.meeds.social social - 7.3.x-SNAPSHOT + 7.3.x-maintenance-SNAPSHOT pom Meeds:: PLF:: Social Meeds Social - Enterprise Social Networking @@ -45,8 +45,8 @@ - 7.3.x-SNAPSHOT - 7.3.x-SNAPSHOT + 7.3.x-maintenance-SNAPSHOT + 7.3.x-maintenance-SNAPSHOT meeds-io diff --git a/webapp/.eslintrc.json b/webapp/.eslintrc.json index 5be9841ab8d..17af6b1bf0d 100644 --- a/webapp/.eslintrc.json +++ b/webapp/.eslintrc.json @@ -14,6 +14,7 @@ "QRCode": true, "DOMPurify": true, "ExtendedDomPurify": true, + "EmojiAccessibility": true, "Cropper": true, "Autolinker": true, "cCometd": true, diff --git a/webapp/pom.xml b/webapp/pom.xml index 4c6ca6afe37..6c440787426 100644 --- a/webapp/pom.xml +++ b/webapp/pom.xml @@ -22,7 +22,7 @@ io.meeds.social social - 7.3.x-SNAPSHOT + 7.3.x-maintenance-SNAPSHOT social-webapp war diff --git a/webapp/src/main/resources/locale/portlet/Portlets_en.properties b/webapp/src/main/resources/locale/portlet/Portlets_en.properties index 6a052faf2e7..f6e6c4c0930 100644 --- a/webapp/src/main/resources/locale/portlet/Portlets_en.properties +++ b/webapp/src/main/resources/locale/portlet/Portlets_en.properties @@ -716,7 +716,8 @@ GroupsManagement.error.invalidField=The "{0}" field must start with a character ##################################################################################### # Search # ##################################################################################### -Search.label.inputPlaceHolder=Search for content... +Search.label.inputPlaceHolder=Start searching content... +Search.label.ariaLabel=Search for content Search.label.inProgress=Search in progress Search.button.tooltip=Search Search.button.tooltip.shortcut=(Ctrl + Shift + f) @@ -807,6 +808,7 @@ Favorite.tooltip.ErrorDeletingFavorite=An error occurred while deleting the {0} Tag.tooltip.startSearch=Start a search based on this tag Tag.search.button=Tags Tag.search.placeholder=Start typing to search a Tag +Tag.search.ariaLabel=Filter by tag name Tag.last.added=Last tags added ##################################################################################### diff --git a/webapp/src/main/resources/locale/portlet/social/PeopleListApplication_en.properties b/webapp/src/main/resources/locale/portlet/social/PeopleListApplication_en.properties index 38bac0c6345..e559cc122d0 100644 --- a/webapp/src/main/resources/locale/portlet/social/PeopleListApplication_en.properties +++ b/webapp/src/main/resources/locale/portlet/social/PeopleListApplication_en.properties @@ -33,6 +33,7 @@ peopleList.title.spacesCount=Spaces peopleList.title.usersToInvite=Invite users peopleList.label.peopleCount=Showing {0} results peopleList.label.filterPeople=Filter by name +peopleList.label.filterPeople.placeholder=Search for a user peopleList.label.filter=Filter peopleList.label.filter.all=Everyone peopleList.label.filter.connections=My Network diff --git a/webapp/src/main/resources/locale/portlet/social/SpacesListApplication_en.properties b/webapp/src/main/resources/locale/portlet/social/SpacesListApplication_en.properties index af647a47b6b..f138baf79f1 100644 --- a/webapp/src/main/resources/locale/portlet/social/SpacesListApplication_en.properties +++ b/webapp/src/main/resources/locale/portlet/social/SpacesListApplication_en.properties @@ -10,6 +10,7 @@ spacesList.label.members={0} Members spacesList.label.oneMember=1 Member spacesList.label.SpaceMembers=Members spacesList.label.filterSpaces=Filter by name or description +spacesList.label.filterSpaces.placeholder=Search for a space spacesList.label.filterSpacesByName=Filter by name spacesList.label.spacesSize={0} spaces listed spacesList.label.ok=OK diff --git a/webapp/src/main/webapp/WEB-INF/gatein-resources.xml b/webapp/src/main/webapp/WEB-INF/gatein-resources.xml index f56b841c355..2dcebc6febc 100644 --- a/webapp/src/main/webapp/WEB-INF/gatein-resources.xml +++ b/webapp/src/main/webapp/WEB-INF/gatein-resources.xml @@ -2829,6 +2829,9 @@ ExtendedDomPurify + + EmojiAccessibility + vue @@ -2849,6 +2852,15 @@ + + EmojiAccessibility + purifyGRP + + + purify purifyGRP diff --git a/webapp/src/main/webapp/js/EmojiAccessibility.js b/webapp/src/main/webapp/js/EmojiAccessibility.js new file mode 100644 index 00000000000..aaf9118736c --- /dev/null +++ b/webapp/src/main/webapp/js/EmojiAccessibility.js @@ -0,0 +1,96 @@ +/** + * This file is part of the Meeds project (https://meeds.io/). + * + * Copyright (C) 2020 - 2022 Meeds Association contact@meeds.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +(function() { + // Accessibility (#4246): expose emojis to screen readers by wrapping + // them in a once their names are loaded. + let emojiNameByChar = null; + let emojiPattern = null; + + function escapeRegExp(value) { + return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + } + + function buildEmojiIndex(emojiBank) { + const map = new Map(); + (emojiBank.categories || []).forEach(category => { + (category.emojis || []).forEach(item => { + if (item.emoji && item.name && !map.has(item.emoji)) { + map.set(item.emoji, item.name); + } + }); + }); + emojiNameByChar = map; + const chars = Array.from(map.keys()).sort((a, b) => b.length - a.length); + emojiPattern = chars.length && new RegExp(chars.map(escapeRegExp).join('|'), 'g'); + } + + let ready = false; + const readyPromise = fetch('/social/json/emojiBank.json?v=1') + .then(resp => resp.ok && resp.json()) + .then(emojiBank => emojiBank && buildEmojiIndex(emojiBank)) + .catch(() => { + // Emoji names not available: emojis will remain unlabeled. + }) + .then(() => { + ready = true; + }); + + function addAccessibleNameToEmojis(html) { + if (!emojiPattern) { + return html; + } + const container = document.createElement('div'); + container.innerHTML = html; + const walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT); + const textNodes = []; + let node; + while ((node = walker.nextNode())) { + emojiPattern.lastIndex = 0; + if (node.parentElement?.getAttribute('role') !== 'img' && emojiPattern.test(node.nodeValue)) { + textNodes.push(node); + } + } + textNodes.forEach(textNode => { + const fragment = document.createDocumentFragment(); + let lastIndex = 0; + let match; + emojiPattern.lastIndex = 0; + while ((match = emojiPattern.exec(textNode.nodeValue))) { + if (match.index > lastIndex) { + fragment.appendChild(document.createTextNode(textNode.nodeValue.slice(lastIndex, match.index))); + } + const span = document.createElement('span'); + span.setAttribute('role', 'img'); + span.setAttribute('aria-label', emojiNameByChar.get(match[0])); + span.textContent = match[0]; + fragment.appendChild(span); + lastIndex = match.index + match[0].length; + } + fragment.appendChild(document.createTextNode(textNode.nodeValue.slice(lastIndex))); + textNode.parentNode.replaceChild(fragment, textNode); + }); + return container.innerHTML; + } + + window.EmojiAccessibility = { + isReady: () => ready, + ready: readyPromise, + addAccessibleNameToEmojis, + }; +})(); diff --git a/webapp/src/main/webapp/js/ExtendedDomPurify.js b/webapp/src/main/webapp/js/ExtendedDomPurify.js index e2ae04e3bd7..552c5b89ae2 100644 --- a/webapp/src/main/webapp/js/ExtendedDomPurify.js +++ b/webapp/src/main/webapp/js/ExtendedDomPurify.js @@ -43,7 +43,7 @@ case 'url' : const url = match.getUrl(); const tag = match.buildTag(); - tag.setAttr('href', encodeURI(url)); + tag.setAttr('href', url); if (match.getUrl().indexOf('/') === 0 || match.getUrl().indexOf(window.location.origin) === 0) { return true; } else { diff --git a/webapp/src/main/webapp/js/sanitize-html-directive.js b/webapp/src/main/webapp/js/sanitize-html-directive.js index 96da9e153f9..9667763d6fe 100644 --- a/webapp/src/main/webapp/js/sanitize-html-directive.js +++ b/webapp/src/main/webapp/js/sanitize-html-directive.js @@ -17,6 +17,14 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ (function() { + function setSanitizedHtml(el, html) { + el.innerHTML = html && EmojiAccessibility.addAccessibleNameToEmojis(html) || ''; + if (html && !EmojiAccessibility.isReady()) { + EmojiAccessibility.ready.then(() => { + el.innerHTML = EmojiAccessibility.addAccessibleNameToEmojis(html); + }); + } + } window.Vue.directive('sanitized-html', function (el, binding) { let content = binding.value; if (content) { @@ -36,7 +44,7 @@ if (!el.classList.contains('reset-style-box')) { el.classList.add('reset-style-box'); } - el.innerHTML = content && ExtendedDomPurify.purify(content) || ''; + setSanitizedHtml(el, content && ExtendedDomPurify.purify(content)); }); window.Vue.directive('sanitized-html-no-embed', function (el, binding) { let content = binding.value; @@ -54,6 +62,6 @@ } } - el.innerHTML = content && ExtendedDomPurify.purify(content) || ''; + setSanitizedHtml(el, content && ExtendedDomPurify.purify(content)); }); })(); diff --git a/webapp/src/main/webapp/vue-apps/activity-stream/components/activity/content/ActivityEmbeddedHTML.vue b/webapp/src/main/webapp/vue-apps/activity-stream/components/activity/content/ActivityEmbeddedHTML.vue index 65d4d603b83..8a39ba54579 100644 --- a/webapp/src/main/webapp/vue-apps/activity-stream/components/activity/content/ActivityEmbeddedHTML.vue +++ b/webapp/src/main/webapp/vue-apps/activity-stream/components/activity/content/ActivityEmbeddedHTML.vue @@ -41,7 +41,13 @@ export default { data: () => ({ maxWidth: 320, elementReady: false, + emojiBankReady: EmojiAccessibility.isReady(), }), + created() { + if (!this.emojiBankReady) { + EmojiAccessibility.ready.then(() => this.emojiBankReady = true); + } + }, computed: { embeddedHTML() { const htmlElement = this.activity?.templateParams?.html; @@ -89,7 +95,8 @@ export default { methods: { computeEmbeddedHTML(htmlElement) { const tempdiv = document.createElement('div'); - tempdiv.innerHTML = ExtendedDomPurify.purify(htmlElement); + const purifiedHtml = ExtendedDomPurify.purify(htmlElement); + tempdiv.innerHTML = this.emojiBankReady && EmojiAccessibility.addAccessibleNameToEmojis(purifiedHtml) || purifiedHtml; if (tempdiv.firstElementChild.style.maxWidth) { tempdiv.firstElementChild.style.maxWidth = `${this.maxWidth}px`; } diff --git a/webapp/src/main/webapp/vue-apps/activity-stream/components/activity/content/ActivityLink.vue b/webapp/src/main/webapp/vue-apps/activity-stream/components/activity/content/ActivityLink.vue index 6154a28ebd0..fa6f8b05106 100644 --- a/webapp/src/main/webapp/vue-apps/activity-stream/components/activity/content/ActivityLink.vue +++ b/webapp/src/main/webapp/vue-apps/activity-stream/components/activity/content/ActivityLink.vue @@ -186,7 +186,8 @@ export default { useEmbeddedLinkView: true, summaryLinesToDisplay: 2, isLandscapeThumbnail: false, - activityViews: null + activityViews: null, + emojiBankReady: EmojiAccessibility.isReady(), }), computed: { showFooter() { @@ -304,13 +305,15 @@ export default { return this.summary && this.$utils.htmlToText(this.summary) || ''; }, summaryElement() { + const purifiedHtml = this.summary && ExtendedDomPurify.purify(`
${this.summary}
`) || ''; return { - template: this.summary && ExtendedDomPurify.purify(`
${this.summary}
`) || '', + template: this.emojiBankReady && purifiedHtml && EmojiAccessibility.addAccessibleNameToEmojis(purifiedHtml) || purifiedHtml, }; }, titleElement() { + const purifiedHtml = this.title && ExtendedDomPurify.purify(`${this.title}`) || ''; return { - template: this.title && ExtendedDomPurify.purify(`${this.title}`) || '', + template: this.emojiBankReady && purifiedHtml && EmojiAccessibility.addAccessibleNameToEmojis(purifiedHtml) || purifiedHtml, }; }, bodyClass() { @@ -370,6 +373,9 @@ export default { created() { this.retrieveActivityProperties(); window.addEventListener('resize', this.displayReadMore); + if (!this.emojiBankReady) { + EmojiAccessibility.ready.then(() => this.emojiBankReady = true); + } }, mounted() { this.displayReadMore(); diff --git a/webapp/src/main/webapp/vue-apps/application-toolbar/components/ApplicationToolbar.vue b/webapp/src/main/webapp/vue-apps/application-toolbar/components/ApplicationToolbar.vue index 3346da32ffb..41d5e06c0d8 100644 --- a/webapp/src/main/webapp/vue-apps/application-toolbar/components/ApplicationToolbar.vue +++ b/webapp/src/main/webapp/vue-apps/application-toolbar/components/ApplicationToolbar.vue @@ -128,26 +128,22 @@ :max-width="rightTextFilter.maxWidth" :class="expandFilter && 'flex-grow-1'" flat> - - - {{ rightTextFilter.tooltip }} - +