0
0
mirror of https://github.com/naturalcrit/homebrewery.git synced 2025-12-31 23:52:48 +00:00

Merge branch 'master' of https://github.com/naturalcrit/homebrewery into add-notification-client-side

This commit is contained in:
Víctor Losada Hernández
2024-10-17 22:29:31 +02:00
15 changed files with 418 additions and 408 deletions

View File

@@ -1,7 +1,7 @@
/*eslint max-lines: ["warn", {"max": 300, "skipBlankLines": true, "skipComments": true}]*/
require('./brewRenderer.less');
const React = require('react');
const { useState, useRef, useEffect, useCallback } = React;
const { useState, useRef, useCallback, useMemo } = React;
const _ = require('lodash');
const MarkdownLegacy = require('naturalcrit/markdownLegacy.js');
@@ -44,7 +44,7 @@ const BrewPage = (props)=>{
//v=====--------------------< Brew Renderer Component >-------------------=====v//
const renderedPages = [];
let renderedPages = [];
let rawPages = [];
const BrewRenderer = (props)=>{
@@ -64,7 +64,6 @@ const BrewRenderer = (props)=>{
};
const [state, setState] = useState({
height : PAGE_HEIGHT,
isMounted : false,
visibility : 'hidden',
zoom : 100
@@ -78,17 +77,6 @@ const BrewRenderer = (props)=>{
rawPages = props.text.split(/^\\page$/gm);
}
useEffect(()=>{ // Unmounting steps
return ()=>{window.removeEventListener('resize', updateSize);};
}, []);
const updateSize = ()=>{
setState((prevState)=>({
...prevState,
height : mainRef.current.parentNode.clientHeight,
}));
};
const updateCurrentPage = useCallback(_.throttle((e)=>{
const { scrollTop, clientHeight, scrollHeight } = e.target;
const totalScrollableHeight = scrollHeight - clientHeight;
@@ -163,8 +151,6 @@ const BrewRenderer = (props)=>{
const frameDidMount = ()=>{ //This triggers when iFrame finishes internal "componentDidMount"
setTimeout(()=>{ //We still see a flicker where the style isn't applied yet, so wait 100ms before showing iFrame
updateSize();
window.addEventListener('resize', updateSize);
renderPages(); //Make sure page is renderable before showing
setState((prevState)=>({
...prevState,
@@ -187,6 +173,15 @@ const BrewRenderer = (props)=>{
}));
};
const styleObject = {};
if(global.config.deployment) {
styleObject.backgroundImage = `url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' version='1.1' height='40px' width='200px'><text x='0' y='15' fill='%23fff7' font-size='20'>${global.config.deployment}</text></svg>")`;
}
const renderedStyle = useMemo(()=> renderStyle(), [props.style?.length, props.themeBundle]);
renderedPages = useMemo(() => renderPages(), [props.text?.length]);
return (
<>
{/*render dummy page while iFrame is mounting.*/}
@@ -212,19 +207,19 @@ const BrewRenderer = (props)=>{
contentDidMount={frameDidMount}
onClick={()=>{emitClick();}}
>
<div className={'brewRenderer'}
<div className={`brewRenderer ${global.config.deployment && 'deployment'}`}
onScroll={updateCurrentPage}
onKeyDown={handleControlKeys}
tabIndex={-1}
style={{ height: state.height }}>
style={ styleObject }>
{/* Apply CSS from Style tab and render pages from Markdown tab */}
{state.isMounted
&&
<>
{renderStyle()}
{renderedStyle}
<div className='pages' lang={`${props.lang || 'en'}`} style={{ zoom: `${state.zoom}%` }}>
{renderPages()}
{renderedPages}
</div>
</>
}

View File

@@ -4,6 +4,10 @@
overflow-y : scroll;
will-change : transform;
padding-top : 30px;
height : 100vh;
&.deployment {
background-color: darkred;
}
:where(.pages) {
margin : 30px 0px;
& > :where(.page) {

View File

@@ -4,7 +4,7 @@ const request = require('../../utils/request-middleware.js');
import Dialog from '../../../components/dialog.jsx';
const DISMISS_KEY = 'dismiss_notification04-09-24';
const DISMISS_KEY = 'dismiss_notification01-10-24';
const DISMISS_BUTTON = <i className='fas fa-times dismiss' />;
const NotificationPopup = ()=>{

View File

@@ -314,7 +314,7 @@ const Editor = createClass({
},
brewJump : function(targetPage=this.props.currentEditorCursorPageNum, smooth=true){
if(!window || isJumping)
if(!window || !this.isText() || isJumping)
return;
// Get current brewRenderer scroll position and calculate target position
@@ -355,7 +355,7 @@ const Editor = createClass({
},
sourceJump : function(targetPage=this.props.currentBrewRendererPageNum, smooth=true){
if(!this.isText || isJumping)
if(!this.isText() || isJumping)
return;
const textSplit = this.props.renderer == 'V3' ? /^\\page$/gm : /\\page/;

View File

@@ -1,11 +1,11 @@
/*eslint max-lines: ["warn", {"max": 250, "skipBlankLines": true, "skipComments": true}]*/
/*eslint max-lines: ["warn", {"max": 350, "skipBlankLines": true, "skipComments": true}]*/
require('./snippetbar.less');
const React = require('react');
const createClass = require('create-react-class');
const _ = require('lodash');
const cx = require('classnames');
import { getHistoryItems, historyExists } from '../../utils/versionHistory.js';
import { loadHistory } from '../../utils/versionHistory.js';
//Import all themes
const ThemeSnippets = {};
@@ -50,30 +50,47 @@ const Snippetbar = createClass({
renderer : this.props.renderer,
themeSelector : false,
snippets : [],
historyExists : false
showHistory : false,
historyExists : false,
historyItems : []
};
},
componentDidMount : async function() {
componentDidMount : async function(prevState) {
const snippets = this.compileSnippets();
this.setState({
snippets : snippets
});
},
componentDidUpdate : async function(prevProps) {
componentDidUpdate : async function(prevProps, prevState) {
if(prevProps.renderer != this.props.renderer || prevProps.theme != this.props.theme || prevProps.snippetBundle != this.props.snippetBundle) {
this.setState({
snippets : this.compileSnippets()
});
};
if(historyExists(this.props.brew) != this.state.historyExists){
// Update history list if it has changed
const checkHistoryItems = await loadHistory(this.props.brew);
// If all items have the noData property, there is no saved data
const checkHistoryExists = !checkHistoryItems.every((historyItem)=>{
return historyItem?.noData;
});
if(prevState.historyExists != checkHistoryExists){
this.setState({
historyExists : !this.state.historyExists
historyExists : checkHistoryExists
});
};
}
// If any history items have changed, update the list
if(checkHistoryExists && checkHistoryItems.some((historyItem, index)=>{
return index >= prevState.historyItems.length || !_.isEqual(historyItem, prevState.historyItems[index]);
})){
this.setState({
historyItems : checkHistoryItems
});
}
},
mergeCustomizer : function(oldValue, newValue, key) {
@@ -151,12 +168,18 @@ const Snippetbar = createClass({
return this.props.updateBrew(item);
},
toggleHistoryMenu : function(){
this.setState({
showHistory : !this.state.showHistory
});
},
renderHistoryItems : function() {
const historyItems = getHistoryItems(this.props.brew);
if(!this.state.historyExists) return;
return <div className='dropdown'>
{_.map(historyItems, (item, index)=>{
if(!item.savedAt) return;
{_.map(this.state.historyItems, (item, index)=>{
if(item.noData || !item.savedAt) return;
const saveTime = new Date(item.savedAt);
const diffMs = new Date() - saveTime;
@@ -197,9 +220,10 @@ const Snippetbar = createClass({
}
return <div className='editors'>
<div className={`editorTool snippetGroup history ${this.state.historyExists ? 'active' : ''}`} >
<div className={`editorTool snippetGroup history ${this.state.historyExists ? 'active' : ''}`}
onClick={this.toggleHistoryMenu} >
<i className='fas fa-clock-rotate-left' />
{this.state.historyExists && this.renderHistoryItems() }
{ this.state.showHistory && this.renderHistoryItems() }
</div>
<div className={`editorTool undo ${this.props.historySize.undo ? 'active' : ''}`}
onClick={this.props.undo} >

View File

@@ -228,8 +228,8 @@ const EditPage = createClass({
htmlErrors : Markdown.validate(prevState.brew.text)
}));
updateHistory(this.state.brew);
versionHistoryGarbageCollection();
await updateHistory(this.state.brew);
await versionHistoryGarbageCollection();
const transfer = this.state.saveGoogle == _.isNil(this.state.brew.googleId);

View File

@@ -172,6 +172,11 @@ const errorIndex = (props)=>{
**Brew Title:** ${props.brew.brewTitle}`,
// ####### Admin page error #######
'52': dedent`
## Access Denied
You need to provide correct administrator credentials to access this page.`,
'90' : dedent` An unexpected error occurred while looking for these brews.
Try again in a few minutes.`,

View File

@@ -1,8 +1,10 @@
import * as IDB from 'idb-keyval/dist/index.js';
export const HISTORY_PREFIX = 'HOMEBREWERY-HISTORY';
export const HISTORY_SLOTS = 5;
// History values in minutes
const DEFAULT_HISTORY_SAVE_DELAYS = {
const HISTORY_SAVE_DELAYS = {
'0' : 0,
'1' : 2,
'2' : 10,
@@ -10,29 +12,30 @@ const DEFAULT_HISTORY_SAVE_DELAYS = {
'4' : 12 * 60,
'5' : 2 * 24 * 60
};
// const HISTORY_SAVE_DELAYS = {
// '0' : 0,
// '1' : 1,
// '2' : 2,
// '3' : 3,
// '4' : 4,
// '5' : 5
// };
const DEFAULT_GARBAGE_COLLECT_DELAY = 28 * 24 * 60;
const HISTORY_SAVE_DELAYS = global.config?.historyData?.HISTORY_SAVE_DELAYS ?? DEFAULT_HISTORY_SAVE_DELAYS;
const GARBAGE_COLLECT_DELAY = global.config?.historyData?.GARBAGE_COLLECT_DELAY ?? DEFAULT_GARBAGE_COLLECT_DELAY;
const HB_DB = 'HOMEBREWERY-DB';
const HB_STORE = 'HISTORY';
const GARBAGE_COLLECT_DELAY = 28 * 24 * 60;
// const GARBAGE_COLLECT_DELAY = 10;
function getKeyBySlot(brew, slot){
// Return a string representing the key for this brew and history slot
return `${HISTORY_PREFIX}-${brew.shareId}-${slot}`;
};
function getVersionBySlot(brew, slot){
// Read stored brew data
// - If it exists, parse data to object
// - If it doesn't exist, pass default object
const key = getKeyBySlot(brew, slot);
const storedVersion = localStorage.getItem(key);
const output = storedVersion ? JSON.parse(storedVersion) : { expireAt: '2000-01-01T00:00:00.000Z', shareId: brew.shareId, noData: true };
return output;
};
function updateStoredBrew(brew, slot = 0) {
function parseBrewForStorage(brew, slot = 0) {
// Strip out unneeded object properties
// Returns an array of [ key, brew ]
const archiveBrew = {
title : brew.title,
text : brew.text,
@@ -46,44 +49,55 @@ function updateStoredBrew(brew, slot = 0) {
archiveBrew.expireAt.setMinutes(archiveBrew.expireAt.getMinutes() + HISTORY_SAVE_DELAYS[slot]);
const key = getKeyBySlot(brew, slot);
localStorage.setItem(key, JSON.stringify(archiveBrew));
return [key, archiveBrew];
}
export function historyExists(brew){
return Object.keys(localStorage)
.some((key)=>{
return key.startsWith(`${HISTORY_PREFIX}-${brew.shareId}`);
});
// Create a custom IDB store
async function createHBStore(){
return await IDB.createStore(HB_DB, HB_STORE);
}
export function loadHistory(brew){
const history = {};
export async function loadHistory(brew){
const DEFAULT_HISTORY_ITEM = { expireAt: '2000-01-01T00:00:00.000Z', shareId: brew.shareId, noData: true };
// Load data from local storage to History object
const historyKeys = [];
// Create array of all history keys
for (let i = 1; i <= HISTORY_SLOTS; i++){
history[i] = getVersionBySlot(brew, i);
historyKeys.push(getKeyBySlot(brew, i));
};
return history;
// Load all keys from IDB at once
const dataArray = await IDB.getMany(historyKeys, await createHBStore());
return dataArray.map((data)=>{ return data ?? DEFAULT_HISTORY_ITEM; });
}
export function updateHistory(brew) {
const history = loadHistory(brew);
export async function updateHistory(brew) {
const history = await loadHistory(brew);
// Walk each version position
for (let slot = HISTORY_SLOTS; slot > 0; slot--){
for (let slot = HISTORY_SLOTS - 1; slot >= 0; slot--){
const storedVersion = history[slot];
// If slot has expired, update all lower slots and break
if(new Date() >= new Date(storedVersion.expireAt)){
for (let updateSlot = slot - 1; updateSlot>0; updateSlot--){
// Create array of arrays : [ [key1, value1], [key2, value2], ..., [keyN, valueN] ]
// to pass to IDB.setMany
const historyUpdate = [];
for (let updateSlot = slot; updateSlot > 0; updateSlot--){
// Move data from updateSlot to updateSlot + 1
!history[updateSlot]?.noData && updateStoredBrew(history[updateSlot], updateSlot + 1);
if(!history[updateSlot - 1]?.noData) {
historyUpdate.push(parseBrewForStorage(history[updateSlot - 1], updateSlot + 1));
}
};
// Update the most recent brew
updateStoredBrew(brew, 1);
historyUpdate.push(parseBrewForStorage(brew, 1));
await IDB.setMany(historyUpdate, await createHBStore());
// Break out of data checks because we found an expired value
break;
@@ -91,26 +105,15 @@ export function updateHistory(brew) {
};
};
export function getHistoryItems(brew){
const historyArray = [];
export async function versionHistoryGarbageCollection(){
for (let i = 1; i <= HISTORY_SLOTS; i++){
historyArray.push(getVersionBySlot(brew, i));
}
const entries = await IDB.entries(await createHBStore());
return historyArray;
};
export function versionHistoryGarbageCollection(){
Object.keys(localStorage)
.filter((key)=>{
return key.startsWith(HISTORY_PREFIX);
})
.forEach((key)=>{
const collectAt = new Date(JSON.parse(localStorage.getItem(key)).savedAt);
collectAt.setMinutes(collectAt.getMinutes() + GARBAGE_COLLECT_DELAY);
if(new Date() > collectAt){
localStorage.removeItem(key);
}
});
for (const [key, value] of entries){
const expireAt = new Date(value.savedAt);
expireAt.setMinutes(expireAt.getMinutes() + GARBAGE_COLLECT_DELAY);
if(new Date() > expireAt){
await IDB.del(key, await createHBStore());
};
};
};