refactor: type view.ts, dom.ts and time.ts

pull/72/head
OFF0 1 year ago
parent 35b8baef92
commit fa97027321
Signed by: offbyn
GPG Key ID: 94A2F643C51F37FA

@ -1,92 +0,0 @@
/**
* example usage:
*
* const props = {className: 'btn', onclick: async (e) => alert('hi')};
* const btn = elem('button', props, ['download']);
* document.body.append(btn);
*
* @param {string} name
* @param {HTMLElement.prototype} props
* @param {Array<HTMLElement|string>} children
* @return HTMLElement
*/
export function elem(name = 'div', {data, ...props} = {}, children = []) {
const el = document.createElement(name);
Object.assign(el, props);
if (['number', 'string'].includes(typeof children)) {
el.append(children);
} else {
el.append(...children);
}
if (data) {
Object.entries(data).forEach(([key, value]) => el.dataset[key] = value);
}
return el;
}
function isValidURL(url) {
if (!['http:', 'https:'].includes(url.protocol)) {
return false;
}
if (!['', '443', '80'].includes(url.port)) {
return false;
}
if (url.hostname === 'localhost') {
return false;
}
const lastDot = url.hostname.lastIndexOf('.');
if (lastDot < 1) {
return false;
}
if (url.hostname.slice(lastDot) === '.local') {
return false;
}
if (url.hostname.slice(lastDot + 1).match(/^[\d]+$/)) { // there should be no tld with numbers, possible ipv4
return false;
}
if (url.hostname.includes(':')) { // possibly an ipv6 addr; certainly an invalid hostname
return false;
}
return true;
}
export function parseTextContent(string) {
let firstLink;
return [string
.trimRight()
.replaceAll(/\n{3,}/g, '\n\n')
.split('\n')
.map(line => {
const words = line.split(/\s/);
return words.map(word => {
if (word.match(/^ln(tbs?|bcr?t?)[a-z0-9]+$/g)) {
return elem('a', {
href: `lightning:${word}`
}, `lightning:${word.slice(0, 24)}`);
}
if (!word.match(/^(https?:\/\/|www\.)\S*/)) {
return word;
}
try {
if (!word.startsWith('http')) {
word = 'https://' + word;
}
const url = new URL(word);
if (!isValidURL(url)) {
return word;
}
firstLink = firstLink || url.href;
return elem('a', {
href: url.href,
target: '_blank',
rel: 'noopener noreferrer'
}, url.href.slice(url.protocol.length + 2));
} catch (err) {
return word;
}
})
.reduce((acc, word) => [...acc, word, ' '], []);
})
.reduce((acc, words) => [...acc, ...words, elem('br')], []),
{firstLink}];
}

@ -1,10 +1,9 @@
import {generatePrivateKey, getEventHash, getPublicKey, nip19, signEvent} from 'nostr-tools';
import {sub24hFeed, subNote, subProfile} from './subscriptions'
import {publish} from './relays';
import {bounce} from './utils.js';
import {clearView, getViewContent, getViewElem, setViewElem, view} from './view';
import {zeroLeadingBitsCount} from './cryptoutils.js';
import {elem, parseTextContent} from './domutil.js';
import {dateTime, formatTime} from './timeutil.js';
import {bounce, dateTime, elem, formatTime, parseTextContent} from './utils';
// curl -H 'accept: application/nostr+json' https://relay.nostr.ch/
function onEvent(evt, relay) {
@ -36,16 +35,6 @@ let pubkey = localStorage.getItem('pub_key') || (() => {
return pubkey;
})();
const containers = [
// {
// id: '/00527c2b28ea78446c148cb40cc6e442ea3d0945ff5a8b71076483398525b54d',
// view: Node,
// content: Node,
// dom: {}
// }
];
let activeContainerIndex = null;
const textNoteList = []; // could use indexDB
const eventRelayMap = {}; // eventId: [relay1, relay2]
@ -180,9 +169,7 @@ const sortByCreatedAt = (evt1, evt2) => {
};
function rerenderFeed() {
const domMap = getViewDom(); // TODO: this is only the current view, do this for all views
Object.keys(domMap).forEach(key => delete domMap[key]);
getViewContent().replaceChildren([]);
clearView();
renderFeed();
}
@ -719,68 +706,11 @@ function updateElemHeight(el) {
function getViewContent() {
return containers[activeContainerIndex]?.content;
}
function getViewDom() {
return containers[activeContainerIndex]?.dom;
}
function getViewElem(key) {
return containers[activeContainerIndex]?.dom[key];
}
function setViewElem(key, node) {
const container = containers[activeContainerIndex];
if (container) {
container.dom[key] = node;
}
return node;
}
const mainContainer = document.querySelector('main');
const getContainer = (containers, route) => {
let container = containers.find(c => c.route === route);
if (container) {
return container;
}
const content = elem('div', {className: 'content'});
const view = elem('section', {className: 'view'}, [content]);
mainContainer.append(view);
container = {route, view, content, dom: {}};
containers.push(container);
return container;
};
document.body.onload = () => console.log('------------ pageload ------------')
function view(route) {
const active = containers[activeContainerIndex];
active?.view.classList.remove('view-active');
const nextContainer = getContainer(containers, route);
const nextContainerIndex = containers.indexOf(nextContainer);
if (nextContainerIndex === activeContainerIndex) {
return;
}
if (active) {
nextContainer.view.classList.add('view-next');
}
requestAnimationFrame(() => {
requestAnimationFrame(() => {
nextContainer.view.classList.remove('view-next', 'view-prev');
nextContainer.view.classList.add('view-active');
});
// // console.log(activeContainerIndex, nextContainerIndex);
getViewContent()?.querySelectorAll('.view-prev').forEach(prev => {
prev.classList.remove('view-prev');
prev.classList.add('view-next');
});
active?.view.classList.add(nextContainerIndex < activeContainerIndex ? 'view-next' : 'view-prev');
activeContainerIndex = nextContainerIndex;
});
}
// subscribe and change view
function route(path) {

@ -1,22 +0,0 @@
/**
* throttle and debounce given function in regular time interval,
* but with the difference that the last call will be debounced and therefore never missed.
* @param {*} function to throttle and debounce
* @param {*} time desired interval to execute function
* @returns callback
*/
export const bounce = (fn, time) => {
let throttle;
let debounce;
return (/*...args*/) => {
if (throttle) {
clearTimeout(debounce);
debounce = setTimeout(() => fn(/*...args*/), time);
return;
}
fn(/*...args*/);
throttle = setTimeout(() => {
throttle = false;
}, time);
};
};

@ -0,0 +1,129 @@
type Attributes = {
[key: string]: string | number;
} & {
data?: {
[key: string]: string | number;
}
};
/**
* example usage:
*
* const props = {className: 'btn', onclick: async (e) => alert('hi')};
* const btn = elem('button', props, ['download']);
* document.body.append(btn);
*
* @param {string} name
* @param {HTMLElement.prototype} props
* @param {Array<HTMLElement|string>} children
* @return HTMLElement
*/
export const elem = (
name: keyof HTMLElementTagNameMap,
attrs: Attributes = {},
children: Array<Node> | string = []
) => {
const {data, ...props} = attrs;
const el = document.createElement(name);
Object.assign(el, props);
if (Array.isArray(children)) {
el.append(...children);
} else {
const childType = typeof children;
if (childType === 'number' || childType === 'string') {
el.append(children);
} else {
console.error('call me');
}
}
if (data) {
Object.entries(data).forEach(([key, value]) => {
el.dataset[key] = value as string;
});
}
return el;
};
export const isValidURL = (url: URL) => {
if (!['http:', 'https:'].includes(url.protocol)) {
return false;
}
if (!['', '443', '80'].includes(url.port)) {
return false;
}
if (url.hostname === 'localhost') {
return false;
}
const lastDot = url.hostname.lastIndexOf('.');
if (lastDot < 1) {
return false;
}
if (url.hostname.slice(lastDot) === '.local') {
return false;
}
if (url.hostname.slice(lastDot + 1).match(/^[\d]+$/)) { // there should be no tld with numbers, possible ipv4
return false;
}
if (url.hostname.includes(':')) { // possibly an ipv6 addr; certainly an invalid hostname
return false;
}
return true;
}
/**
* example usage:
*
* const [content, {firstLink}] = parseTextContent('Hi<br>click https://nostr.ch/');
*
* @param {string} content
* @returns [Array<string | HTMLElement>, {firstLink: href}]
*/
export const parseTextContent = (
content: string,
): [
Array<string | HTMLAnchorElement | HTMLBRElement>,
{firstLink: string | undefined},
] => {
let firstLink: string | undefined;
const parsedContent = content
.trim()
.replaceAll(/\n{3,}/g, '\n\n')
.split('\n')
.map(line => {
const words = line.split(/\s/);
return words.map(word => {
if (word.match(/^ln(tbs?|bcr?t?)[a-z0-9]+$/g)) {
return elem('a', {
href: `lightning:${word}`
}, `lightning:${word.slice(0, 24)}`);
}
if (!word.match(/^(https?:\/\/|www\.)\S*/)) {
return word;
}
try {
if (!word.startsWith('http')) {
word = 'https://' + word;
}
const url = new URL(word);
if (!isValidURL(url)) {
return word;
}
firstLink = firstLink || url.href;
return elem('a', {
href: url.href,
target: '_blank',
rel: 'noopener noreferrer'
}, url.href.slice(url.protocol.length + 2));
} catch (err) {
return word;
}
})
.reduce((acc, word) => [...acc, word, ' '], []);
})
.reduce((acc, words) => [...acc, ...words, elem('br')], []);
return [
parsedContent,
{firstLink}
];
};

@ -0,0 +1,2 @@
export {elem, parseTextContent} from './dom';
export {bounce, dateTime, formatTime} from './time';

@ -1,8 +1,34 @@
/**
* throttle and debounce given function in regular time interval,
* but with the difference that the last call will be debounced and therefore never missed.
* @param {*} function to throttle and debounce
* @param {*} time desired interval to execute function
* @returns callback
*/
export const bounce = (
fn: () => void,
time: number,
) => {
let throttle;
let debounce;
return (/*...args*/) => {
if (throttle) {
clearTimeout(debounce);
debounce = setTimeout(() => fn(/*...args*/), time);
return;
}
fn(/*...args*/);
throttle = setTimeout(() => {
throttle = false;
}, time);
};
};
/**
* Intl.DateTimeFormat object
*
*
* example:
*
*
* console.log(dateTime.format(new Date()));
*/
export const dateTime = new Intl.DateTimeFormat('de-ch' /* navigator.language */, {
@ -12,17 +38,20 @@ export const dateTime = new Intl.DateTimeFormat('de-ch' /* navigator.language */
/**
* format time relative to now, such as 5min ago
*
* @param {Date} time
*
* @param {Date} time
* @param {string} locale
* @returns string
*
*
* example:
*
*
* console.log(timeAgo(new Date(Date.now() - 10000)));
*
*
*/
const timeAgo = (time, locale = 'en') => {
const timeAgo = (
time: Date,
locale: string = 'en',
) => {
const relativeTime = new Intl.RelativeTimeFormat(locale, {
numeric: 'auto',
style: 'long',
@ -55,7 +84,7 @@ const timeAgo = (time, locale = 'en') => {
* @param {time} date object to format
* @return string
*/
export const formatTime = (time) => {
export const formatTime = (time: Date) => {
const yesterday = new Date(Date.now() - (24 * 60 * 60 * 1000));
if (time > yesterday) {
return timeAgo(time);

@ -0,0 +1,76 @@
import {elem} from './utils';
type Container = {
id: string;
view: HTMLSelectElement;
content: HTMLDivElement;
dom: {
[eventId: string]: HTMLElement
}
};
const containers: Array<Container> = [];
let activeContainerIndex = -1;
export const getViewContent = () => containers[activeContainerIndex]?.content;
export const clearView = () => {
// TODO: this is clears the current view, but it should probably do this for all views
const domMap = containers[activeContainerIndex]?.dom;
Object.keys(domMap).forEach(eventId => delete domMap[eventId]);
getViewContent().replaceChildren();
};
export const getViewElem = (eventId: string) => {
return containers[activeContainerIndex]?.dom[eventId];
};
export const setViewElem = (eventId: string, node: HTMLElement) => {
const container = containers[activeContainerIndex];
if (container) {
container.dom[eventId] = node;
}
return node;
};
const mainContainer = document.querySelector('main');
const getContainer = (route: string) => {
let container = containers.find(c => c.id === route);
if (container) {
return container;
}
const content = elem('div', {className: 'content'});
const view = elem('section', {className: 'view'}, [content]);
mainContainer?.append(view);
container = {id: route, view, content, dom: {}};
containers.push(container);
return container;
};
export const view = (route: string) => {
const active = containers[activeContainerIndex];
active?.view.classList.remove('view-active');
const nextContainer = getContainer(route);
const nextContainerIndex = containers.indexOf(nextContainer);
if (nextContainerIndex === activeContainerIndex) {
return;
}
if (active) {
nextContainer.view.classList.add('view-next');
}
requestAnimationFrame(() => {
requestAnimationFrame(() => {
nextContainer.view.classList.remove('view-next', 'view-prev');
nextContainer.view.classList.add('view-active');
});
// // console.log(activeContainerIndex, nextContainerIndex);
getViewContent()?.querySelectorAll('.view-prev').forEach(prev => {
prev.classList.remove('view-prev');
prev.classList.add('view-next');
});
active?.view.classList.add(nextContainerIndex < activeContainerIndex ? 'view-next' : 'view-prev');
activeContainerIndex = nextContainerIndex;
});
};

@ -0,0 +1,6 @@
{
"compilerOptions": {
"target": "es2021"
},
"exclude": ["node_modules", "dist", "**/*.test.ts"]
}
Loading…
Cancel
Save