Notifications Initializer Migration to Pack tag (#19124)

Co-authored-by: Ridhwana <Ridhwana.Khan16@gmail.com>
Co-authored-by: Lawrence S <lboogie@Lawrences-Air.attlocal.net>
Co-authored-by: Rajat Talesra <rajat@forem.com>
This commit is contained in:
Lawrence 2023-04-14 16:50:07 -05:00 committed by GitHub
parent 2233f2865b
commit 56e10f1306
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 285 additions and 73 deletions

View file

@ -2,7 +2,7 @@
global initializeLocalStorageRender, initializeBodyData,
initializeAllTagEditButtons, initializeUserFollowButts,
initializeCommentsPage,
initializeArticleDate, initializeArticleReactions, initNotifications,
initializeArticleDate, initializeArticleReactions,
initializeSettings, initializeRuntimeBanner,
initializeTimeFixer, initializeDashboardSort, initializeCreditsPage,
initializeDrawerSliders, initializeHeroBannerClose,
@ -17,7 +17,6 @@ function callInitializers() {
initializeCommentsPage();
initializeArticleDate();
initializeArticleReactions();
initNotifications();
initializeSettings();
initializeTimeFixer();
initializeDashboardSort();

View file

@ -1,5 +1,14 @@
import { initializeCommentDate } from './initializers/initializeCommentDate';
import { initializeCommentPreview } from './initializers/initializeCommentPreview';
import { initializeNotifications } from './initializers/initializeNotifications';
import {
showUserAlertModal,
showModalAfterError,
} from '@utilities/showUserAlertModal';
initializeCommentDate();
initializeCommentPreview();
initializeNotifications();
window.showUserAlertModal = showUserAlertModal;
window.showModalAfterError = showModalAfterError;

View file

@ -1,17 +1,10 @@
/* global checkUserLoggedIn, instantClick, InstantClick, sendHapticMessage, showModalAfterError */
function initNotifications() {
fetchNotificationsCount();
markNotificationsAsRead();
initReactions();
listenForNotificationsBellClick();
initFilter();
initPagination();
initLoadMoreButton();
}
import { sendHapticMessage } from '../../utilities/sendHapticMessage';
import { checkUserLoggedIn } from '../../utilities/checkUserLoggedIn';
import { showModalAfterError } from '../../utilities/showUserAlertModal';
// eslint-disable-next-line no-redeclare
/* global InstantClick, instantClick */
function markNotificationsAsRead() {
setTimeout(function () {
setTimeout(() => {
if (document.getElementById('notifications-container')) {
getCsrfToken().then((csrfToken) => {
const locationAsArray = window.location.pathname.split('/');
@ -44,7 +37,7 @@ function fetchNotificationsCount() {
// Prefetch notifications page
if (instantClick) {
InstantClick.removeExpiredKeys('force');
setTimeout(function () {
setTimeout(() => {
InstantClick.preload(
document.getElementById('notifications-link').href,
'force',
@ -55,18 +48,18 @@ function fetchNotificationsCount() {
}
function initReactions() {
setTimeout(function () {
setTimeout(() => {
if (document.getElementById('notifications-container')) {
var butts = document.getElementsByClassName('reaction-button');
let butts = document.getElementsByClassName('reaction-button');
for (var i = 0; i < butts.length; i++) {
var butt = butts[i];
for (let i = 0; i < butts.length; i++) {
const butt = butts[i];
butt.setAttribute('aria-pressed', butt.classList.contains('reacted'));
butt.onclick = function (event) {
event.preventDefault();
sendHapticMessage('medium');
var thisButt = this;
const thisButt = this;
thisButt.classList.add('reacted');
function successCb(response) {
@ -79,14 +72,14 @@ function initReactions() {
}
}
var formData = new FormData();
const formData = new FormData();
formData.append('reactable_type', thisButt.dataset.reactableType);
formData.append('category', thisButt.dataset.category);
formData.append('reactable_id', thisButt.dataset.reactableId);
getCsrfToken()
.then(sendFetch('reaction-creation', formData))
.then(function (response) {
.then((response) => {
if (response.status === 200) {
response.json().then(successCb);
} else {
@ -108,16 +101,16 @@ function initReactions() {
butt.onclick = function (event) {
event.preventDefault();
var thisButt = this;
const thisButt = this;
document
.getElementById('comment-form-for-' + thisButt.dataset.reactableId)
.getElementById(`comment-form-for-${ thisButt.dataset.reactableId }`)
.classList.remove('hidden');
thisButt.classList.add('hidden');
thisButt.classList.remove('inline-flex');
setTimeout(function () {
setTimeout(() => {
document
.getElementById(
'comment-textarea-for-' + thisButt.dataset.reactableId,
`comment-textarea-for-${ thisButt.dataset.reactableId }`,
)
.focus();
}, 30);
@ -128,9 +121,9 @@ function initReactions() {
}
function listenForNotificationsBellClick() {
var notificationsLink = document.getElementById('notifications-link');
const notificationsLink = document.getElementById('notifications-link');
if (notificationsLink) {
setTimeout(function () {
setTimeout(() => {
notificationsLink.onclick = function () {
document.getElementById('notifications-number').classList.add('hidden');
};
@ -162,9 +155,9 @@ function initPagination() {
method: 'GET',
credentials: 'same-origin',
})
.then(function (response) {
.then((response) => {
if (response.status === 200) {
response.text().then(function (html) {
response.text().then((html) => {
const markup = html.trim();
if (markup) {
@ -198,3 +191,13 @@ function initLoadMoreButton() {
button.addEventListener('click', initPagination);
}
}
export function initializeNotifications() {
fetchNotificationsCount();
markNotificationsAsRead();
initReactions();
listenForNotificationsBellClick();
initFilter();
initPagination();
initLoadMoreButton();
}

View file

@ -0,0 +1,14 @@
import { checkUserLoggedIn } from '@utilities/checkUserLoggedIn';
describe('CheckUserLoggedIn Utility', () => {
it('should return false if no body', () => {
const userLoggedIn = checkUserLoggedIn();
expect(userLoggedIn).toEqual(false);
});
it('should return true if user has the logged in attribute', () => {
document.body.setAttribute('data-user-status', 'logged-in');
const userLoggedIn = checkUserLoggedIn();
expect(userLoggedIn).toEqual(true);
});
});

View file

@ -0,0 +1,24 @@
import { sendHapticMessage } from '../sendHapticMessage';
describe('SendHapticMessage Utility', () => {
it('should call postMessage', async () => {
const mockPostMessage = jest.fn();
global.window.webkit = {
messageHandlers: {
haptic: {
postMessage: mockPostMessage,
},
},
};
await sendHapticMessage('sample message');
expect(mockPostMessage).toHaveBeenCalled();
});
it('should log to console otherwise', async () => {
global.window = {};
global.console.log = jest.fn();
await sendHapticMessage('sample message');
expect(global.console.log).not.toHaveBeenCalled();
});
});

View file

@ -0,0 +1,73 @@
import 'isomorphic-fetch';
import {
getModalHtml,
showModalAfterError,
} from '../../utilities/showUserAlertModal';
describe('ShowUserAlert Utility', () => {
beforeEach(() => {
const mockShowModal = jest.fn();
global.window.Forem = { showModal: mockShowModal };
});
it('should return modal html', () => {
const modalHtml = getModalHtml('Sample text', 'Sample Confirm Text');
expect(modalHtml).toContain('Sample text');
});
test('shows rate limit modal if response status is 429', async () => {
const response = new Response('', { status: 429 });
const showRateLimitModal = jest.fn();
const showUserAlertModal = jest.fn();
await showModalAfterError({
response,
element: 'post',
action_ing: 'creating',
action_past: 'created',
timeframe: '5 minutes',
showRateLimitModal,
showUserAlertModal,
});
expect(showUserAlertModal).not.toHaveBeenCalled();
});
test('shows user alert modal if response status is not 429', async () => {
const response = new Response(
JSON.stringify({ error: 'Something went wrong' }),
);
const showRateLimitModal = jest.fn();
const showUserAlertModal = jest.fn();
await showModalAfterError({
response,
element: 'post',
action_ing: 'creating',
action_past: 'created',
timeframe: '5 minutes',
showRateLimitModal,
showUserAlertModal,
});
expect(showRateLimitModal).not.toHaveBeenCalled();
});
test('shows user alert modal if response cannot be parsed as JSON', async () => {
const response = new Response('Something went wrong', { status: 500 });
const showRateLimitModal = jest.fn();
const showUserAlertModal = jest.fn();
await showModalAfterError({
response,
element: 'post',
action_ing: 'creating',
action_past: 'created',
timeframe: '5 minutes',
showRateLimitModal,
showUserAlertModal,
});
expect(showRateLimitModal).not.toHaveBeenCalled();
});
});

View file

@ -0,0 +1,8 @@
export function checkUserLoggedIn() {
const { body } = document;
if (!body) {
return false;
}
return body.getAttribute('data-user-status') === 'logged-in';
}

View file

@ -0,0 +1,14 @@
export function sendHapticMessage(message) {
try {
if (
window &&
window.webkit &&
window.webkit.messageHandlers &&
window.webkit.messageHandlers.haptic
) {
window.webkit.messageHandlers.haptic.postMessage(message);
}
} catch (err) {
console.log(err.message); // eslint-disable-line no-console
}
}

View file

@ -18,7 +18,7 @@ const modalId = 'user-alert-modal';
* @example
* showUserAlertModal('Warning', 'You must wait', 'OK', '/faq/why-must-i-wait', 'Why must I wait?');
*/
function showUserAlertModal(title, text, confirm_text) {
export function showUserAlertModal(title, text, confirm_text) {
buildModalDiv(text, confirm_text);
window.Forem.showModal({
title,
@ -26,6 +26,7 @@ function showUserAlertModal(title, text, confirm_text) {
overlay: true,
});
}
/**
* Displays a user rate limit alert modal letting the user know what they did that exceeded a rate limit,
* and gives them links to explain why they must wait
@ -45,13 +46,13 @@ function showRateLimitModal({
action_past,
timeframe = 'a moment',
}) {
let rateLimitText = buildRateLimitText({
const rateLimitText = buildRateLimitText({
element,
action_ing,
action_past,
timeframe,
});
let rateLimitLink = '/faq';
const rateLimitLink = '/faq';
showUserAlertModal(
`Wait ${timeframe}...`,
rateLimitText,
@ -60,6 +61,7 @@ function showRateLimitModal({
'Why do I have to wait?',
);
}
/**
* Displays the corresponding modal after an error.
*
@ -73,7 +75,7 @@ function showRateLimitModal({
* @example
* showModalAfterError(response, 'made a comment', 'making another comment', 'a moment');
*/
function showModalAfterError({
export function showModalAfterError({
response,
element,
action_ing,
@ -82,19 +84,23 @@ function showModalAfterError({
}) {
response
.json()
.then(function parseError(errorResponse) {
.then((errorResponse) => {
if (response.status === 429) {
showRateLimitModal({ element, action_ing, action_past, timeframe });
showRateLimitModal({
element,
action_ing,
action_past,
timeframe,
});
} else {
showUserAlertModal(
`Error ${action_ing} ${element}`,
`Your ${element} could not be ${action_past} due to an error: ` +
errorResponse.error,
`Your ${element} could not be ${action_past} due to an error: ${errorResponse.error}`,
'OK',
);
}
})
.catch(function parseError(error) {
.catch(() => {
showUserAlertModal(
`Error ${action_ing} ${element}`,
`Your ${element} could not be ${action_past} due to a server error`,
@ -103,30 +109,6 @@ function showModalAfterError({
});
}
/**
* HTML template for modal
*
* @private
* @function getModalHtml
*
* @param {string} text The body text to be displayed
* @param {string} confirm_text Text of the confirmation button
*
* @returns {string} HTML for the modal
*/
const getModalHtml = (text, confirm_text) => `
<div id="${modalId}" hidden>
<div class="flex flex-col">
<p class="color-base-70">
${text}
</p>
<button class="crayons-btn mt-4 ml-auto" type="button" onClick="window.Forem.closeModal()">
${confirm_text}
</button>
</div>
</div>
`;
/**
* Constructs wording for rate limit modals
*
@ -140,10 +122,39 @@ const getModalHtml = (text, confirm_text) => `
*
* @returns {string} Formatted body text for a rate limit modal
*/
function buildRateLimitText({ element, action_ing, action_past, timeframe }) {
export function buildRateLimitText({
element,
action_ing,
action_past,
timeframe,
}) {
return `Since you recently ${action_past} a ${element}, youll need to wait ${timeframe} before ${action_ing} another ${element}.`;
}
/**
* HTML template for modal
*
* @private
* @function getModalHtml
*
* @param {string} text The body text to be displayed
* @param {string} confirm_text Text of the confirmation button
*
* @returns {string} HTML for the modal
*/
export const getModalHtml = (text, confirm_text) => `
<div id="${modalId}" hidden>
<div class="flex flex-col">
<p class="color-base-70">
${text}
</p>
<button class="crayons-btn mt-4 ml-auto" type="button" onClick="window.Forem.closeModal()">
${confirm_text}
</button>
</div>
</div>
`;
/**
* Checks for the alert modal, and if it's not present builds and inserts it in the DOM
*
@ -177,8 +188,8 @@ function buildModalDiv(text, confirm_text) {
*
* @returns {Element} DOM node of alert modal with formatted text
*/
function getModal(text, confirm_text) {
let wrapper = document.createElement('div');
export function getModal(text, confirm_text) {
const wrapper = document.createElement('div');
wrapper.innerHTML = getModalHtml(text, confirm_text);
return wrapper;
}

View file

@ -0,0 +1,42 @@
describe('Notification initialization', () => {
describe('Home page notifications', () => {
beforeEach(() => {
cy.testSetup();
cy.fixture('users/notificationsUser.json').as('user');
cy.get('@user').then((user) => {
cy.loginUser(user).then(() => {
cy.visit('/');
});
});
});
it('Shows the notification count', () => {
cy.get('#notifications-link').find('span').as('pan');
cy.get('@pan').should('have.text', '2');
});
});
describe('Notifications page', () => {
beforeEach(() => {
cy.testSetup();
cy.fixture('users/notificationsUser.json').as('user');
cy.get('@user').then((user) => {
cy.loginAndVisit(user, '/notifications');
});
});
it('Shows the notifications marked as read', () => {
cy.get('#notifications-link').find('span').as('pan');
cy.get('@pan').should('have.value', '');
});
it('initializes reactions', () => {
cy.get('.reaction-button').should('exist');
cy.get('.reacted').should('exist');
});
});
});

View file

@ -82,6 +82,7 @@
"eslint-plugin-no-only-tests": "^3.1.0",
"eslint-plugin-react": "^7.32.2",
"husky": "^8.0.3",
"isomorphic-fetch": "^3.0.0",
"jest": "28.1.3",
"jest-axe": "^6.0.0",
"jest-environment-jsdom": "^28.1.3",
@ -131,6 +132,7 @@
"he": "^1.2.0",
"i18n-js": "^4.2.3",
"intersection-observer": "^0.12.2",
"isomorphic-fetch": "^3.0.0",
"jest-junit": "^14.0.1",
"linkstate": "^2.0.1",
"lodash.debounce": "4.0.8",

View file

@ -5463,14 +5463,14 @@ caniuse-api@^3.0.0:
lodash.uniq "^4.5.0"
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000981, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001304, caniuse-lite@^1.0.30001400, caniuse-lite@^1.0.30001426:
version "1.0.30001434"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001434.tgz#ec1ec1cfb0a93a34a0600d37903853030520a4e5"
integrity sha512-aOBHrLmTQw//WFa2rcF1If9fa3ypkC1wzqqiKHgfdrXTWcU8C4gKVZT77eQAPWN1APys3+uQ0Df07rKauXGEYA==
version "1.0.30001474"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001474.tgz#13b6fe301a831fe666cce8ca4ef89352334133d5"
integrity sha512-iaIZ8gVrWfemh5DG3T9/YqarVZoYf0r188IjaGwx68j4Pf0SGY6CQkmJUIE+NZHkkecQGohzXmBGEwWDr9aM3Q==
canvas@^2.11.0:
version "2.11.0"
resolved "https://registry.yarnpkg.com/canvas/-/canvas-2.11.0.tgz#7f0c3e9ae94cf469269b5d3a7963a7f3a9936434"
integrity sha512-bdTjFexjKJEwtIo0oRx8eD4G2yWoUOXP9lj279jmQ2zMnTQhT8C3512OKz3s+ZOaQlLbE7TuVvRDYDB3Llyy5g==
version "2.11.2"
resolved "https://registry.yarnpkg.com/canvas/-/canvas-2.11.2.tgz#553d87b1e0228c7ac0fc72887c3adbac4abbd860"
integrity sha512-ItanGBMrmRV7Py2Z+Xhs7cT+FNt5K0vPL4p9EZ/UX/Mu7hFbkxSjKF2KVtPwX7UYWp7dRKnrTvReflgrItJbdw==
dependencies:
"@mapbox/node-pre-gyp" "^1.0.0"
nan "^2.17.0"
@ -9918,6 +9918,14 @@ isobject@^4.0.0:
resolved "https://registry.yarnpkg.com/isobject/-/isobject-4.0.0.tgz#3f1c9155e73b192022a80819bacd0343711697b0"
integrity sha512-S/2fF5wH8SJA/kmwr6HYhK/RI/OkhD84k8ntalo0iJjZikgq1XFvR5M8NPT1x5F7fBwCG3qHfnzeP/Vh/ZxCUA==
isomorphic-fetch@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz#0267b005049046d2421207215d45d6a262b8b8b4"
integrity sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==
dependencies:
node-fetch "^2.6.1"
whatwg-fetch "^3.4.1"
isomorphic-unfetch@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/isomorphic-unfetch/-/isomorphic-unfetch-3.1.0.tgz#87341d5f4f7b63843d468438128cb087b7c3e98f"
@ -16840,6 +16848,11 @@ whatwg-encoding@^2.0.0:
dependencies:
iconv-lite "0.6.3"
whatwg-fetch@^3.4.1:
version "3.6.2"
resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz#dced24f37f2624ed0281725d51d0e2e3fe677f8c"
integrity sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA==
whatwg-mimetype@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz#5fa1a7623867ff1af6ca3dc72ad6b8a4208beba7"