deicidus
3 years ago
commit
aab65a0daf
8 changed files with 3126 additions and 0 deletions
@ -0,0 +1,122 @@ |
|||||||
|
const satsPerBtc = 100000000 // one hundred million per btc
|
||||||
|
import { createHash } from './crypto.js' |
||||||
|
import { Task } from './types.js' |
||||||
|
|
||||||
|
export function crawlerHash(tasks: Task[], taskId: string) { |
||||||
|
const crawlerResults = crawler(tasks, taskId) |
||||||
|
let buffers: number[] = [] |
||||||
|
crawlerResults.forEach(tId => { |
||||||
|
buffers.push(...Buffer.from(tId)) |
||||||
|
}) |
||||||
|
const bufferResult = Buffer.from(buffers) |
||||||
|
return createHash(bufferResult) |
||||||
|
} |
||||||
|
|
||||||
|
export function crawler(tasks: Task[], taskId: string): string[] { |
||||||
|
let history: string[] = [] |
||||||
|
tasks.forEach(task => { |
||||||
|
if (task.taskId === taskId) { |
||||||
|
let crawler: string[] = [taskId] |
||||||
|
do { |
||||||
|
let newCards: string[] = [] |
||||||
|
crawler.forEach(t => { |
||||||
|
if (history.indexOf(t) >= 0) return |
||||||
|
history.push(t) |
||||||
|
let subTask = tasks.filter(pst => pst.taskId === t)[0] |
||||||
|
if (subTask) { |
||||||
|
let gridCells = |
||||||
|
subTask.pins && subTask.pins.length >= 1 |
||||||
|
? subTask.pins.map(pin => pin.taskId) |
||||||
|
: [] |
||||||
|
newCards = newCards |
||||||
|
.concat(subTask.subTasks) |
||||||
|
.concat(subTask.priorities) |
||||||
|
.concat(subTask.completed) |
||||||
|
.concat(gridCells) |
||||||
|
} |
||||||
|
}) |
||||||
|
crawler = newCards |
||||||
|
} while (crawler.length > 0) |
||||||
|
} |
||||||
|
}) |
||||||
|
return history |
||||||
|
} |
||||||
|
|
||||||
|
export function shortName(name) { |
||||||
|
let limit = 280 |
||||||
|
let shortened = name.substring(0, limit) |
||||||
|
if (name.length > limit) { |
||||||
|
shortened += '…' |
||||||
|
} |
||||||
|
return shortened |
||||||
|
} |
||||||
|
|
||||||
|
export function cardColorCSS(color) { |
||||||
|
return { |
||||||
|
redwx: color == 'red', |
||||||
|
bluewx: color == 'blue', |
||||||
|
greenwx: color == 'green', |
||||||
|
yellowwx: color == 'yellow', |
||||||
|
purplewx: color == 'purple', |
||||||
|
blackwx: color == 'black', |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export function isString(x) { |
||||||
|
return Object.prototype.toString.call(x) === '[object String]' |
||||||
|
} |
||||||
|
|
||||||
|
export function cadToSats(cadAmt, spot) { |
||||||
|
let sats = (parseFloat(cadAmt) / parseFloat(spot)) * satsPerBtc |
||||||
|
return Math.floor(sats) |
||||||
|
} |
||||||
|
|
||||||
|
export function satsToCad(sats, spot) { |
||||||
|
let cad = sats * (spot / satsPerBtc) |
||||||
|
return cad.toFixed(2) |
||||||
|
} |
||||||
|
|
||||||
|
export function calculateMsThisMonth() { |
||||||
|
let today = new Date() |
||||||
|
let daysThisMonth = new Date( |
||||||
|
today.getFullYear(), |
||||||
|
today.getMonth(), |
||||||
|
0 |
||||||
|
).getDate() |
||||||
|
return daysThisMonth * 24 * 60 * 60 * 1000 |
||||||
|
} |
||||||
|
|
||||||
|
export function getMeridienTime(ts) { |
||||||
|
let d = new Date(parseInt(ts)) |
||||||
|
let hour24 = d.getHours() |
||||||
|
|
||||||
|
let rollover = 0 |
||||||
|
if (hour24 >= 24) { |
||||||
|
rollover = 1 |
||||||
|
hour24 %= 24 |
||||||
|
} |
||||||
|
|
||||||
|
let hour, meridien |
||||||
|
if (hour24 > 12) { |
||||||
|
meridien = 'pm' |
||||||
|
hour = hour24 - 12 |
||||||
|
} else { |
||||||
|
meridien = 'am' |
||||||
|
hour = hour24 |
||||||
|
} |
||||||
|
|
||||||
|
let date = d.getDate() + rollover |
||||||
|
let month = d.getMonth() + 1 |
||||||
|
let minute = d.getMinutes() |
||||||
|
let year = d.getFullYear() |
||||||
|
|
||||||
|
let weekday = d.toString().slice(0, 3) |
||||||
|
|
||||||
|
return { weekday, year, month, date, hour, minute, meridien } |
||||||
|
} |
||||||
|
|
||||||
|
export function toTitleCase(str) { |
||||||
|
return str.replace(/\w\S*/g, function (txt) { |
||||||
|
return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase() |
||||||
|
}) |
||||||
|
} |
@ -0,0 +1,798 @@ |
|||||||
|
import { isString } from './calculations.js' |
||||||
|
import { v1 } from 'uuid' |
||||||
|
import { |
||||||
|
Task, |
||||||
|
Signature, |
||||||
|
UserSeen, |
||||||
|
CardZone, |
||||||
|
Coords, |
||||||
|
CardLocation, |
||||||
|
Pinboard, |
||||||
|
PinboardStyle, |
||||||
|
CardPass, |
||||||
|
} from './types.js' |
||||||
|
|
||||||
|
// With this set to 1, actions will occur immediately, as if they were not potential-based actions
|
||||||
|
export const POTENTIALS_TO_EXECUTE = 1 |
||||||
|
|
||||||
|
// Default style when a new pinboard is created without specifiying the style
|
||||||
|
const defaultPinboardStyle: PinboardStyle = 'pyramid' |
||||||
|
|
||||||
|
// Grid squares have a width measure in ems. The default number for this is 9 and the +/- buttons increase or decrease this by 1.
|
||||||
|
const defaultSquareSizeInEms = 9 |
||||||
|
|
||||||
|
// The one and only function to create a new blank card, please use it everywhere cards are created for standardization and searchability.
|
||||||
|
export function blankCard( |
||||||
|
taskId, |
||||||
|
name, |
||||||
|
color, |
||||||
|
created, |
||||||
|
deck = [], |
||||||
|
parents = [], |
||||||
|
height = undefined, |
||||||
|
width = undefined |
||||||
|
): Task { |
||||||
|
let newCard = { |
||||||
|
taskId, |
||||||
|
color, |
||||||
|
deck, |
||||||
|
name: typeof name !== 'string' ? 'invalid filename' : name.trim(), |
||||||
|
address: '', // Could be left undefined by default?
|
||||||
|
bolt11: '', // Could be left undefined by default?
|
||||||
|
book: undefined, // Could be left undefined by default?
|
||||||
|
boost: 0, |
||||||
|
priorities: [], |
||||||
|
subTasks: [], |
||||||
|
completed: [], |
||||||
|
pinboard: |
||||||
|
height && width && height >= 1 && width >= 1 |
||||||
|
? blankPinboard(height, width) |
||||||
|
: null, |
||||||
|
pins: [], |
||||||
|
parents: parents, |
||||||
|
claimed: [], |
||||||
|
passed: [], |
||||||
|
signed: [], // Could be left undefined by default?
|
||||||
|
guild: false, |
||||||
|
created: created, |
||||||
|
lastClaimed: 0, |
||||||
|
payment_hash: '', // Could be left undefined by default?
|
||||||
|
highlights: [], |
||||||
|
seen: deck.length >= 1 ? [{ memberId: deck[0], timestamp: created }] : [], |
||||||
|
time: [], |
||||||
|
allocations: [], |
||||||
|
} |
||||||
|
return newCard |
||||||
|
} |
||||||
|
|
||||||
|
// Returns a blank pinboard of the default style
|
||||||
|
export function blankPinboard( |
||||||
|
height = 3, |
||||||
|
width = 3, |
||||||
|
spread = defaultPinboardStyle |
||||||
|
): Pinboard { |
||||||
|
const newGrid = { |
||||||
|
spread: spread, |
||||||
|
height: height, |
||||||
|
width: width, |
||||||
|
size: defaultSquareSizeInEms, |
||||||
|
} |
||||||
|
return newGrid |
||||||
|
} |
||||||
|
|
||||||
|
// Version of this function for the server, merge with above
|
||||||
|
export function allReachableHeldParentsServer(tasks, origin, memberId) { |
||||||
|
if (!origin?.hasOwnProperty('taskId')) { |
||||||
|
return [] |
||||||
|
} |
||||||
|
let queue = [origin] |
||||||
|
let reachableCards = [] |
||||||
|
|
||||||
|
let visited = {} |
||||||
|
visited[origin.taskId] = true |
||||||
|
let i = 0 |
||||||
|
while (queue.length >= 1) { |
||||||
|
let task = queue.pop() |
||||||
|
if ( |
||||||
|
task === undefined || |
||||||
|
task.subTasks === undefined || |
||||||
|
task.priorities === undefined || |
||||||
|
task.completed === undefined |
||||||
|
) { |
||||||
|
console.log('Invalid task found during returned cards search, skipping.') |
||||||
|
continue |
||||||
|
} |
||||||
|
|
||||||
|
if (task.deck.indexOf(memberId) < 0 && task.taskId !== memberId) { |
||||||
|
continue |
||||||
|
} |
||||||
|
|
||||||
|
reachableCards.push(task) |
||||||
|
if (task.hasOwnProperty('parents') && task.parents.length >= 1) { |
||||||
|
let parents = tasks.filter(taskItem => |
||||||
|
task.parents.includes(taskItem.taskId) |
||||||
|
) |
||||||
|
parents.forEach(st => { |
||||||
|
if (!st.hasOwnProperty('taskId')) { |
||||||
|
console.log('Missing parent found during returned cards search.') |
||||||
|
return |
||||||
|
} |
||||||
|
if (!visited.hasOwnProperty(st.taskId)) { |
||||||
|
visited[st.taskId] = true |
||||||
|
queue.push(st) |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return reachableCards |
||||||
|
} |
||||||
|
|
||||||
|
let dupesGlossary = {} |
||||||
|
// Adds a synonym taskId for another taskId to the duplicates glossary
|
||||||
|
export function registerDuplicateTaskId(originalTaskId, duplicateTaskId) { |
||||||
|
if (!dupesGlossary[duplicateTaskId]) { |
||||||
|
dupesGlossary[duplicateTaskId] = originalTaskId |
||||||
|
console.log(Object.keys(dupesGlossary).length, 'cards with duplicates') |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Returns the task with the given taskId from the given list of tasks, or null
|
||||||
|
// Uses the duplicates glossary to return synonymous tasks that were created by junk task-created events
|
||||||
|
export function getTask(tasks, taskId) { |
||||||
|
// Look up duplicate tasks in the duplicates glossary to politely overlook duplicate task-created mutations
|
||||||
|
let loops = 0 |
||||||
|
while (dupesGlossary[taskId] && loops < 4) { |
||||||
|
taskId = dupesGlossary[taskId] |
||||||
|
//console.log("Looked up duplicate task:", taskId, " (", Object.keys(dupesGlossary).length, " duplicated)")
|
||||||
|
loops++ |
||||||
|
} |
||||||
|
if (loops >= 4) { |
||||||
|
console.log( |
||||||
|
'Woah, four or more redirects in the duplicates glossary, something weird' |
||||||
|
) |
||||||
|
} |
||||||
|
return tasks.find(task => { |
||||||
|
if (task.taskId === taskId) { |
||||||
|
return task |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
// Returns the first task that exactly matches the given value of the given property
|
||||||
|
// Todo: move property to be the first argument
|
||||||
|
export function getTaskBy(tasks, value, property) { |
||||||
|
return tasks.find(task => { |
||||||
|
if (task[property] === value) { |
||||||
|
return task |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
// Returns true if the given taskId matches an existing task/card in the state/database
|
||||||
|
// The state should match the database exactly,
|
||||||
|
// because it is sourced from the database via the deterministic event mutations in mutations.js
|
||||||
|
export function taskExists(tasks, taskId) { |
||||||
|
return tasks.some(task => task.taskId === taskId) |
||||||
|
} |
||||||
|
|
||||||
|
// Marks the task as seen by the given memberId
|
||||||
|
export function seeTask(task, memberId) { |
||||||
|
if (!task.seen) { |
||||||
|
task.seen = [] |
||||||
|
} |
||||||
|
task.seen = task.seen.filter(seenObject => seenObject.memberId !== memberId) |
||||||
|
task.seen.push({ memberId: memberId, timestamp: Date.now() }) |
||||||
|
} |
||||||
|
|
||||||
|
// Clears any pending passes to the specified memberId from the task
|
||||||
|
// This is done whenever a member accepts a pass or grabs or handles a card
|
||||||
|
export function clearPassesTo(tasks, task, memberId, alsoClearFrom = false) { |
||||||
|
const lengthBefore = task.passed.length |
||||||
|
task.passed = task.passed.filter( |
||||||
|
d => d[1] !== memberId || (alsoClearFrom ? d[0] !== memberId : false) |
||||||
|
) |
||||||
|
if (lengthBefore != task.passed.length) { |
||||||
|
const memberCard = getTask(tasks, memberId) |
||||||
|
changeGiftCount(memberCard, task.passed.length - lengthBefore) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Takes a member card and increases or decreases its .giftCount property, adding it if necessary
|
||||||
|
export function changeGiftCount(memberCard, amount) { |
||||||
|
if (!memberCard) { |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
if (!memberCard.hasOwnProperty('giftCount')) { |
||||||
|
memberCard.giftCount = 0 |
||||||
|
} |
||||||
|
|
||||||
|
memberCard.giftCount += amount |
||||||
|
|
||||||
|
if (memberCard.giftCount < 0) { |
||||||
|
memberCard.giftCount = 0 |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Grabs a task, adding it to the member's deck
|
||||||
|
// The associated pending pass on the card, if any, will be removed
|
||||||
|
// You cannot grab your own member card
|
||||||
|
export function grabTask(tasks, task, memberId) { |
||||||
|
clearPassesTo(tasks, task, memberId) |
||||||
|
if (memberId && task.deck.indexOf(memberId) === -1) { |
||||||
|
if (task.taskId !== memberId) { |
||||||
|
task.deck.push(memberId) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Drops the task, removing it from the member's deck
|
||||||
|
export function dropTask(task, memberId) { |
||||||
|
task.deck = task.deck.filter(d => d !== memberId) |
||||||
|
} |
||||||
|
|
||||||
|
// Adds the given taskId to list of the card's parents (this is a cache)
|
||||||
|
export function addParent(task, parentId) { |
||||||
|
if (!task.hasOwnProperty('parents') || !Array.isArray(task.parents)) { |
||||||
|
task.parents = [] |
||||||
|
} |
||||||
|
if (!task.parents.some(pId => pId === parentId)) { |
||||||
|
task.parents.push(parentId) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Removes the given taskId from the list of the card's parents
|
||||||
|
// This function seems to make no sense
|
||||||
|
export function removeParent(task, parentId) { |
||||||
|
if (!task.hasOwnProperty('parents') || !Array.isArray(task.parents)) { |
||||||
|
return |
||||||
|
} |
||||||
|
let gridCells = [] |
||||||
|
if (task.pins && task.pins.length >= 1) { |
||||||
|
gridCells = task.pins.map(pin => pin.taskId) |
||||||
|
} |
||||||
|
|
||||||
|
let stashItems: string[] = [] |
||||||
|
if (task.stash && Object.keys(task.stash).length >= 1) { |
||||||
|
stashItems = [...Object.values<string>(task.stash)] |
||||||
|
} |
||||||
|
|
||||||
|
const allSubTasks = [ |
||||||
|
...task.priorities, |
||||||
|
...task.subTasks, |
||||||
|
...gridCells, |
||||||
|
...task.completed, |
||||||
|
...stashItems, |
||||||
|
] |
||||||
|
|
||||||
|
if (!allSubTasks.some(stId => stId === parentId)) { |
||||||
|
task.parents = task.parents.filter(tId => tId !== parentId) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Removes the second card from the first card's list of parents,
|
||||||
|
// unless the card is actuall still a parent
|
||||||
|
export function removeParentIfNotParent(task, parent) { |
||||||
|
if ( |
||||||
|
!task.hasOwnProperty('parents') || |
||||||
|
!Array.isArray(task.parents) || |
||||||
|
task.parents.length < 1 |
||||||
|
) { |
||||||
|
return |
||||||
|
} |
||||||
|
let gridCells = [] |
||||||
|
if (parent.pins && parent.pins.length >= 1) { |
||||||
|
gridCells = parent.pins.map(pin => pin.taskId) |
||||||
|
} |
||||||
|
|
||||||
|
let stashItems: string[] = [] |
||||||
|
if (parent.stash && Object.keys(parent.stash).length >= 1) { |
||||||
|
stashItems = [...Object.values<string>(parent.stash)] |
||||||
|
} |
||||||
|
|
||||||
|
const allSubTasks = [ |
||||||
|
...parent.priorities, |
||||||
|
...parent.subTasks, |
||||||
|
...gridCells, |
||||||
|
...parent.completed, |
||||||
|
...stashItems, |
||||||
|
] |
||||||
|
|
||||||
|
if (!allSubTasks.some(stId => stId === task.taskId)) { |
||||||
|
task.parents = task.parents.filter(tId => tId !== parent.taskId) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Removes the given taskId from the priorities, subTasks, and completed of the given task
|
||||||
|
// Does NOT remove the taskId from the grid
|
||||||
|
export function filterFromSubpiles(task, taskId) { |
||||||
|
const start = [ |
||||||
|
task?.priorities?.length || null, |
||||||
|
task?.subTask?.length || null, |
||||||
|
task?.completed?.length || null, |
||||||
|
] |
||||||
|
discardPriority(task, taskId) |
||||||
|
discardSubTask(task, taskId) |
||||||
|
discardCompletedTask(task, taskId) |
||||||
|
if ( |
||||||
|
(start[0] !== null && start[0] - task?.priorities?.length > 0) || |
||||||
|
(start[1] !== null && start[1] - task?.subTasks?.length > 0) || |
||||||
|
(start[2] !== null && start[2] - task?.completed?.length > 0) |
||||||
|
) { |
||||||
|
return true |
||||||
|
} |
||||||
|
return false |
||||||
|
} |
||||||
|
|
||||||
|
// Marks as unseen (clears seen from) the given task, unless it's on the list
|
||||||
|
// Unseens bubble-up one level, but if you've seen the child, it doesn't affect you
|
||||||
|
export function clearSeenExcept(task, exceptionMemberIds: UserSeen[] = []) { |
||||||
|
if (task?.seen?.length >= 1) { |
||||||
|
task.seen = task.seen.filter(userseen => |
||||||
|
exceptionMemberIds.includes(userseen.memberId) |
||||||
|
) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Re-adds the given taskId to the given card's subTasks (moving it to the end)
|
||||||
|
// This will move it to the top/front of the list of cards in the GUI
|
||||||
|
// Precondition: The subtask referred to by subTaskId exists (otherwise it will create a broken card link / missing reference)
|
||||||
|
export function addSubTask(task, subTaskId) { |
||||||
|
if (!task) { |
||||||
|
console.log( |
||||||
|
'Attempting to add a subtask to a missing task, this should never happen' |
||||||
|
) |
||||||
|
return |
||||||
|
} |
||||||
|
discardSubTask(task, subTaskId) |
||||||
|
task.subTasks.push(subTaskId) |
||||||
|
} |
||||||
|
|
||||||
|
// Removes the given discardTaskId from the given task's subtasks
|
||||||
|
function discardSubTask(task, discardTaskId) { |
||||||
|
if (!task || !discardTaskId || !task.subTasks || task.subTasks.length <= 0) |
||||||
|
return |
||||||
|
task.subTasks = task.subTasks.filter(stId => stId !== discardTaskId) |
||||||
|
} |
||||||
|
|
||||||
|
// Removes the given discardTaskId from the given task's completed tasks list
|
||||||
|
function discardCompletedTask(task, discardTaskId) { |
||||||
|
if (!task || !discardTaskId || !task.completed || task.completed.length <= 0) |
||||||
|
return |
||||||
|
task.completed = task.completed.filter(stId => stId !== discardTaskId) |
||||||
|
} |
||||||
|
|
||||||
|
// Adds a completed task to the completed list in a card or moves it to the top of the list
|
||||||
|
function addCompletedTask(task, completedTaskId) { |
||||||
|
discardCompletedTask(task, completedTaskId) |
||||||
|
task.completed.push(completedTaskId) |
||||||
|
} |
||||||
|
|
||||||
|
// Adds the subTask to the given new parent task and adds the parent as a parent to the new subTask
|
||||||
|
export function putTaskInTask(subTask, inTask) { |
||||||
|
addSubTask(inTask, subTask.taskId) |
||||||
|
addParent(subTask, inTask.taskId) |
||||||
|
} |
||||||
|
|
||||||
|
// Re-adds the given taskId to the given card's priorities (moving it to the end)
|
||||||
|
// This will move it to the top/front of the list of cards in the GUI
|
||||||
|
export function addPriority(task, taskId) { |
||||||
|
discardPriority(task, taskId) |
||||||
|
task.priorities.push(taskId) |
||||||
|
} |
||||||
|
|
||||||
|
// Removes the given discardTaskId from the given task's subtasks
|
||||||
|
function discardPriority(task, discardTaskId) { |
||||||
|
if ( |
||||||
|
!task || |
||||||
|
!discardTaskId || |
||||||
|
!task.priorities || |
||||||
|
task.priorities.length <= 0 |
||||||
|
) |
||||||
|
return |
||||||
|
task.priorities = task.priorities.filter(stId => stId !== discardTaskId) |
||||||
|
} |
||||||
|
|
||||||
|
export function unpinTasksOutOfBounds(tasks, task) { |
||||||
|
if (!tasks || !task || !task.pins || task.pins.length <= 0) { |
||||||
|
return |
||||||
|
} |
||||||
|
const vertLimit = task.pinboard.spread === 'rune' ? 1 : task.pinboard.height |
||||||
|
for (let i = task.pins.length - 1; i >= 0; i--) { |
||||||
|
const pin = task.pins[i] |
||||||
|
const { taskId, y, x } = pin |
||||||
|
const horizLimit = |
||||||
|
task.pinboard.spread === 'pyramid' ? y + 1 : task.pinboard.width |
||||||
|
if (x >= horizLimit || y >= vertLimit) { |
||||||
|
const theSubTask = getTask(tasks, taskId) |
||||||
|
unpinTaskFromTask(task, { y: y, x: x }) |
||||||
|
if (theSubTask) { |
||||||
|
putTaskInTask(theSubTask, task) |
||||||
|
} else { |
||||||
|
console.log('A missing card was removed from the pinboard:', taskId) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Unpins the card from the given coordinates in a card and returns its taskId
|
||||||
|
function unpinTaskFromTask(task, coords) { |
||||||
|
let result |
||||||
|
if (!task.pins || task.pins.length <= 0) { |
||||||
|
return null |
||||||
|
} |
||||||
|
task.pins.some((pin, i) => { |
||||||
|
const { pinId, y, x } = pin |
||||||
|
if (y == coords.y && x == coords.x) { |
||||||
|
result = task.pins.splice(i, 1)[0] |
||||||
|
} |
||||||
|
}) |
||||||
|
return result |
||||||
|
} |
||||||
|
|
||||||
|
// Precondition: The spec should validate whether this is a legal move based upon the current gridStyle of the card
|
||||||
|
// In other words, this function does not check if the coordinates are off the side of the pinboard
|
||||||
|
// Unlike the functions to add subtasks, this function does NOT attempt to filter the pinboard before adding a card,
|
||||||
|
// so duplicates will occur unless you unpin first from the origin coords
|
||||||
|
// However, it WILL check where the card is going to be placed, and if a card is already there, that card will drop into .subTasks
|
||||||
|
function pinTaskToTask(task, taskId, coords) { |
||||||
|
if (!task.hasOwnProperty('pins') || !Array.isArray(task.pins)) { |
||||||
|
task.pins = [] |
||||||
|
} |
||||||
|
|
||||||
|
// If this taskId is already at this location, do nothing
|
||||||
|
if ( |
||||||
|
task.pins.some( |
||||||
|
pin => pin.taskId === taskId && pin.y === coords.y && pin.x === coords.x |
||||||
|
) |
||||||
|
) { |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// If there is already something pinned there, drop it into subTasks
|
||||||
|
const previousPinnedTaskId = unpinTaskFromTask(task, coords)?.taskId |
||||||
|
|
||||||
|
if (previousPinnedTaskId) { |
||||||
|
addSubTask(task, previousPinnedTaskId) |
||||||
|
} |
||||||
|
|
||||||
|
task.pins.push({ taskId, y: coords.y, x: coords.x }) |
||||||
|
} |
||||||
|
|
||||||
|
function putTaskInTaskZone(task, inTask, toLocation) { |
||||||
|
switch (toLocation.zone) { |
||||||
|
case 'priorities': |
||||||
|
// Move the card to the .priorities
|
||||||
|
//filterFromSubpiles(inTask, task.taskId)
|
||||||
|
addPriority(inTask, task.taskId) |
||||||
|
addParent(task, inTask.taskId) |
||||||
|
break |
||||||
|
case 'grid': |
||||||
|
// Move the card to the .pins using coordinates in the current gridStyle, or fail if not possible
|
||||||
|
// If there isn't a grid on this card, add a grid large enough for the new coordinates to fit on
|
||||||
|
if (!inTask.pinboard) { |
||||||
|
inTask.pinboard = blankPinboard( |
||||||
|
Math.max(toLocation.coords.y, 3), |
||||||
|
Math.max(toLocation.coords.x, 3) |
||||||
|
) |
||||||
|
} |
||||||
|
pinTaskToTask(inTask, task.taskId, toLocation.coords) |
||||||
|
addParent(task, inTask.taskId) |
||||||
|
break |
||||||
|
case 'completed': |
||||||
|
// Move the card to the .completed
|
||||||
|
addCompletedTask(inTask, task.taskId) |
||||||
|
break |
||||||
|
case 'discard': |
||||||
|
// Remove the card from its inId, or save in .completed if it's
|
||||||
|
// Could replace task-de-sub-tasked
|
||||||
|
filterFromSubpiles(inTask, task.taskId) |
||||||
|
break |
||||||
|
case 'context': |
||||||
|
case 'panel': |
||||||
|
// These don't do anything on the server, it's a zone only on the client
|
||||||
|
break |
||||||
|
case 'gifts': |
||||||
|
// Deprecated?
|
||||||
|
break |
||||||
|
case 'stash': |
||||||
|
// the .level on the toLocation.level tells what stash level to put the card in
|
||||||
|
// Rethink this
|
||||||
|
break |
||||||
|
case 'card': |
||||||
|
case 'subTasks': |
||||||
|
default: |
||||||
|
// Move the card to the .subTasks (replaces task-sub-tasked)
|
||||||
|
putTaskInTask(task, inTask) |
||||||
|
break |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Removes the specified discardTaskId from the specified zone of the given task
|
||||||
|
// If zone argument is 'card' or empty, tries to discard from priorities, subTasks, and completed (but not grid)
|
||||||
|
export function discardTaskFromZone(task, fromLocation) { |
||||||
|
switch (fromLocation.zone) { |
||||||
|
case 'grid': |
||||||
|
unpinTaskFromTask(task, fromLocation.coords) |
||||||
|
return |
||||||
|
case 'priorities': |
||||||
|
discardPriority(task, fromLocation.taskId) |
||||||
|
return |
||||||
|
case 'completed': |
||||||
|
discardCompletedTask(task, fromLocation.taskId) |
||||||
|
return |
||||||
|
case 'subTasks': |
||||||
|
discardSubTask(task, fromLocation.taskId) |
||||||
|
return |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Moves a card from one location to another location.
|
||||||
|
// fromLocation defines the card to be unplayed from somewhere, and toLocation defines a card to be placed somewhere.
|
||||||
|
// fromLocation and toLocation are CardLocation objects defining a taskId in a location.
|
||||||
|
// The fromLocation is an optional CardLocation that, if provided, requires a taskId and zone at minimum
|
||||||
|
// If null, no card will be unplayed.
|
||||||
|
// fromLocation.taskId and toLocation.taskId can be different,
|
||||||
|
// so it is possible to play a different card than was unplayed in one move (i.e., swap out a card)
|
||||||
|
// Right now the card being played must exist; card creation and modification is separate since it includes color etc.
|
||||||
|
// Maybe toLocation should be option also, simplifying discards and further decomposing a play.
|
||||||
|
export function atomicCardPlay( |
||||||
|
tasks, |
||||||
|
fromLocation: CardLocation, |
||||||
|
toLocation: CardLocation, |
||||||
|
memberId |
||||||
|
) { |
||||||
|
const taskId = |
||||||
|
fromLocation && fromLocation.taskId |
||||||
|
? fromLocation.taskId |
||||||
|
: toLocation.taskId |
||||||
|
const theCard = getTask(tasks, taskId) |
||||||
|
if (!theCard && fromLocation?.zone !== 'grid') { |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
const theCardMovedTo = getTask(tasks, toLocation.inId) |
||||||
|
if ( |
||||||
|
!theCardMovedTo && |
||||||
|
!['discard', 'context', 'panel'].includes(toLocation.zone) |
||||||
|
) { |
||||||
|
console.log( |
||||||
|
'Attempting to move a card to a missing card, this should never happen. Missing card:', |
||||||
|
toLocation.inId, |
||||||
|
'and zone:', |
||||||
|
toLocation.zone |
||||||
|
) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
if (theCard && memberId) { |
||||||
|
// memberId should be required, but temporarily for debugging
|
||||||
|
// You cannot play a card without having seen it
|
||||||
|
seeTask(theCard, memberId) |
||||||
|
|
||||||
|
// You cannot play a card without first grabbing it
|
||||||
|
grabTask(tasks, theCard, memberId) |
||||||
|
} |
||||||
|
|
||||||
|
// Remove the card from wherever it was moved from
|
||||||
|
const fromInId = fromLocation?.inId |
||||||
|
const theCardMovedFrom = getTask(tasks, fromInId) |
||||||
|
if (theCardMovedFrom) { |
||||||
|
discardTaskFromZone(theCardMovedFrom, fromLocation) |
||||||
|
if (fromLocation.inId !== toLocation.inId) { |
||||||
|
removeParentIfNotParent(theCard, theCardMovedFrom) |
||||||
|
} |
||||||
|
|
||||||
|
// Save the card to the completed cards if it has at least one checkmark
|
||||||
|
if ( |
||||||
|
fromLocation.zone != 'completed' && |
||||||
|
toLocation.zone === 'discard' && |
||||||
|
theCard && |
||||||
|
theCard.claimed && |
||||||
|
theCard.claimed.length >= 1 |
||||||
|
) { |
||||||
|
addCompletedTask(theCardMovedFrom, taskId) |
||||||
|
} |
||||||
|
|
||||||
|
// If removing from priorities, remove allocations that were on the card from theCardMovedFrom
|
||||||
|
if ( |
||||||
|
fromLocation?.zone === 'priorities' && |
||||||
|
toLocation.zone !== 'priorities' && |
||||||
|
theCardMovedFrom.allocations && |
||||||
|
Array.isArray(theCardMovedFrom.allocations) |
||||||
|
) { |
||||||
|
theCardMovedFrom.allocations = theCardMovedFrom.allocations.filter(al => { |
||||||
|
if (al.allocatedId === taskId) { |
||||||
|
theCardMovedFrom.boost += al.amount |
||||||
|
return false |
||||||
|
} |
||||||
|
return true |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Move card to wherever it was moved to
|
||||||
|
if (theCard) { |
||||||
|
putTaskInTaskZone(theCard, theCardMovedTo, toLocation) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Adds the given taskId to the given card's stash of the specified level
|
||||||
|
// Each membership level added to a card has a corresponding stash level
|
||||||
|
export function stashTask(task, taskId, level) { |
||||||
|
if ( |
||||||
|
!task.hasOwnProperty('stash') || |
||||||
|
!( |
||||||
|
typeof task.stash === 'object' && |
||||||
|
task.stash !== null && |
||||||
|
!Array.isArray(task.stash) |
||||||
|
) |
||||||
|
) { |
||||||
|
task.stash = {} |
||||||
|
} |
||||||
|
if (!task.stash.hasOwnProperty('level')) { |
||||||
|
task.stash[level] = [] |
||||||
|
} |
||||||
|
task.stash[level] = task.stash[level].filter(tId => tId !== taskId) |
||||||
|
task.stash[level].push(taskId) |
||||||
|
} |
||||||
|
|
||||||
|
// Removes the given taskId from the given card's stash of the specified level
|
||||||
|
export function unstashTask(task, taskId, level) { |
||||||
|
if ( |
||||||
|
!task.hasOwnProperty('stash') || |
||||||
|
!( |
||||||
|
typeof task.stash === 'object' && |
||||||
|
task.stash !== null && |
||||||
|
!Array.isArray(task.stash) |
||||||
|
) |
||||||
|
) { |
||||||
|
return |
||||||
|
} |
||||||
|
if (!task.stash.hasOwnProperty('level')) { |
||||||
|
return |
||||||
|
} |
||||||
|
task.stash[level] = task.stash[level].filter(tId => tId !== taskId) |
||||||
|
} |
||||||
|
|
||||||
|
// A potentials list is a list of signatures, each signature endorsing a specific task event-type
|
||||||
|
// When POTENTIALS_TO_EXECUTE potentials accrue for a given event-type it is executed, like an action potential
|
||||||
|
// This allows built-in AO mutations to be voted upon by members before execution
|
||||||
|
// Because it is a vote, duplicate potentials for the same event-type are prevented
|
||||||
|
export function addPotential(member, signature) { |
||||||
|
if (!member.potentials) { |
||||||
|
member.potentials = [] |
||||||
|
} |
||||||
|
|
||||||
|
member.potentials = member.potentials.filter( |
||||||
|
pot => |
||||||
|
!( |
||||||
|
pot.opinion === signature.opinion && pot.memberId === signature.memberId |
||||||
|
) |
||||||
|
) |
||||||
|
|
||||||
|
member.potentials.push(signature) |
||||||
|
} |
||||||
|
|
||||||
|
// Returns true if there are POTENTIALS_TO_EXECUTE or more potentials of the specified event-type on the object
|
||||||
|
export function checkPotential(member, eventType) { |
||||||
|
return ( |
||||||
|
member.potentials.filter(pot => pot.opinion === eventType).length >= |
||||||
|
POTENTIALS_TO_EXECUTE |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
// Clears all potentials of the specified event-type from the given card
|
||||||
|
export function clearPotential(member, eventType) { |
||||||
|
member.potentials = member.potentials.filter(pot => pot.opinion !== eventType) |
||||||
|
} |
||||||
|
|
||||||
|
// Sets the lastUsed property of the given object to the given timestamp
|
||||||
|
export function updateLastUsed(member, timestamp) { |
||||||
|
member.lastUsed = timestamp |
||||||
|
} |
||||||
|
|
||||||
|
export function safeMerge(cardA, cardZ) { |
||||||
|
if (!cardA || !cardZ) { |
||||||
|
console.log('attempt to merge nonexistent card') |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
if (!cardZ.taskId || !isString(cardZ.taskId)) { |
||||||
|
console.log('attempt to merge card with a missing or invalid taskId') |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
if (!cardZ.color) { |
||||||
|
console.log('attempt to merge card without a color') |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
if (isString(cardZ.color) && cardZ.color.trim().length >= 1) { |
||||||
|
cardA.color = cardZ.color |
||||||
|
} |
||||||
|
|
||||||
|
if (isString(cardZ.guild) && cardZ.color.trim().length >= 1) { |
||||||
|
cardA.guild = cardZ.guild |
||||||
|
} |
||||||
|
|
||||||
|
const filterNull = tasks => { |
||||||
|
return tasks.filter(task => task !== null && task !== undefined) |
||||||
|
} |
||||||
|
|
||||||
|
cardA.book = cardZ.book |
||||||
|
cardA.address = cardZ.address |
||||||
|
cardA.bolt11 = cardZ.bolt11 |
||||||
|
cardA.priorities = [ |
||||||
|
...new Set(cardA.priorities.concat(filterNull(cardZ.priorities))), |
||||||
|
] |
||||||
|
cardA.subTasks = [ |
||||||
|
...new Set(cardA.subTasks.concat(filterNull(cardZ.subTasks))), |
||||||
|
] |
||||||
|
cardA.completed = [ |
||||||
|
...new Set(cardA.completed.concat(filterNull(cardZ.completed))), |
||||||
|
] |
||||||
|
|
||||||
|
// Replace the pinboard (maybe they could merge? or at least drop existing pins down to subTasks)
|
||||||
|
if ( |
||||||
|
cardZ.pinboard && |
||||||
|
cardZ.pinboard.height >= 1 && |
||||||
|
cardZ.pinboard.width >= 1 |
||||||
|
) { |
||||||
|
if (!cardA.pinboard) { |
||||||
|
cardA.pinboard = blankPinboard() |
||||||
|
} |
||||||
|
cardA.pinboard.height = Math.max( |
||||||
|
cardA.pinboard.height, |
||||||
|
cardZ.pinboard.height |
||||||
|
) |
||||||
|
cardA.pinboard.width = Math.max(cardA.pinboard.width, cardZ.pinboard.width) |
||||||
|
cardA.pinboard.spread = cardZ.pinboard.spread |
||||||
|
if (cardZ.pins && Array.isArray(cardZ.pins)) { |
||||||
|
cardA.pins = cardZ.pins |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
cardA.passed = [...new Set([...cardA.passed, ...filterNull(cardZ.passed)])] |
||||||
|
// Remove duplicate passes
|
||||||
|
let passesNoDuplicates: CardPass[] = [] |
||||||
|
cardA.passed.forEach(pass => { |
||||||
|
if ( |
||||||
|
!passesNoDuplicates.some( |
||||||
|
pass2 => pass[0] === pass2[0] && pass[1] === pass2[1] |
||||||
|
) |
||||||
|
) { |
||||||
|
passesNoDuplicates.push(pass) |
||||||
|
} |
||||||
|
}) |
||||||
|
cardA.passed = passesNoDuplicates |
||||||
|
|
||||||
|
// XXX only add in merge for now
|
||||||
|
// XXX bolt11 / address need to clearly indicate origin ao
|
||||||
|
// XXX book should be a list?
|
||||||
|
} |
||||||
|
|
||||||
|
// A card's .signed is an append-only list of all signing events.
|
||||||
|
// This function reduces it to just each member's current opinion
|
||||||
|
// signed is type Signature[]
|
||||||
|
export function mostRecentSignaturesOnly(signed) { |
||||||
|
let mostRecentSignaturesOnly = signed.filter((signature, index) => { |
||||||
|
let lastIndex |
||||||
|
for (let i = signed.length - 1; i >= 0; i--) { |
||||||
|
if (signed[i].memberId === signature.memberId) { |
||||||
|
lastIndex = i |
||||||
|
break |
||||||
|
} |
||||||
|
} |
||||||
|
return lastIndex === index |
||||||
|
}) |
||||||
|
return mostRecentSignaturesOnly |
||||||
|
} |
||||||
|
|
||||||
|
// Signed is type Signature[]
|
||||||
|
export function countCurrentSignatures(signed) { |
||||||
|
return mostRecentSignaturesOnly(signed).filter( |
||||||
|
signature => signature.opinion >= 1 |
||||||
|
).length |
||||||
|
} |
@ -0,0 +1,30 @@ |
|||||||
|
import crypto from 'crypto' // Does not work on client because this is a Node library, but works for below server-only functions
|
||||||
|
// These libraries are old but they work and can be included on both server and client
|
||||||
|
import shajs from 'sha.js' |
||||||
|
import hmac from 'hash.js/lib/hash/hmac.js' |
||||||
|
import sha256 from 'hash.js/lib/hash/sha/256.js' // Only works for shorter hashes, not in createHash used for hashing meme files
|
||||||
|
|
||||||
|
export function createHash(payload) { |
||||||
|
return shajs('sha256').update(payload).digest('hex') |
||||||
|
} |
||||||
|
|
||||||
|
export function hmacHex(data, signingKey) { |
||||||
|
return hmac(sha256, signingKey).update(data).digest('hex') |
||||||
|
} |
||||||
|
|
||||||
|
export function derivePublicKey(p) { |
||||||
|
return crypto.createPublicKey(p).export({ |
||||||
|
type: 'spki', |
||||||
|
format: 'pem', |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
export function encryptToPublic(pub, info) { |
||||||
|
return crypto.publicEncrypt(pub, new Buffer(info)).toString('hex') |
||||||
|
} |
||||||
|
|
||||||
|
export function decryptFromPrivate(priv, hiddenInfo) { |
||||||
|
return crypto |
||||||
|
.privateDecrypt(priv, Buffer.from(hiddenInfo, 'hex')) |
||||||
|
.toString('latin1') |
||||||
|
} |
@ -0,0 +1,100 @@ |
|||||||
|
import { getTask } from './cards.js' |
||||||
|
|
||||||
|
// DUPLICATED function from client/cardActions.ts
|
||||||
|
// Returns the number of vouches a member has. Member card must exist for each voucher. Members do not have to be active to vouch.
|
||||||
|
// This function can be used either on the client or the server. If on the server, server state must be passed as the second arg.
|
||||||
|
export function countVouches(memberId, state): number | null { |
||||||
|
let card |
||||||
|
card = getTask(state.tasks, memberId) |
||||||
|
if (!card || !card.hasOwnProperty('deck')) return null |
||||||
|
|
||||||
|
let count = 0 |
||||||
|
|
||||||
|
const memberCards = card.deck |
||||||
|
.map(memberId => state.members.find(m => m.memberId === memberId)) |
||||||
|
.forEach(memberCard => { |
||||||
|
if (memberCard !== undefined) { |
||||||
|
count++ |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
return count |
||||||
|
} |
||||||
|
|
||||||
|
// Returns true if the senpai memberId is ahead of the kohai memberId in the members list order
|
||||||
|
export function isAheadOf(senpaiId, kohaiId, state, errRes?) { |
||||||
|
if (errRes === undefined) { |
||||||
|
errRes = [] |
||||||
|
} |
||||||
|
let senpaiRank = state.members.findIndex(m => m.memberId === senpaiId) |
||||||
|
let kohaiRank = state.members.findIndex(m => m.memberId === kohaiId) |
||||||
|
if (senpaiRank < kohaiRank) { |
||||||
|
return 1 |
||||||
|
} else if (kohaiRank < senpaiRank) { |
||||||
|
return -1 |
||||||
|
} |
||||||
|
|
||||||
|
errRes.push('member is not ahead of other member in order of member list') |
||||||
|
return 0 |
||||||
|
} |
||||||
|
|
||||||
|
// Returns true if the senpai has more attack than the kohai has defense
|
||||||
|
// A member's defense is their number of vouches, or the highest attack score out of anyone who vouches for them (whichever is greater)
|
||||||
|
// This method does not check if vouchers exist, therefore it depends on the mutations being perfect
|
||||||
|
// and there not being any invalid members leftover in the .deck / vouchers list of the other member
|
||||||
|
export function isDecidedlyMorePopularThan(senpaiId, kohaiId, state, errRes?) { |
||||||
|
if (errRes === undefined) { |
||||||
|
errRes = [] |
||||||
|
} |
||||||
|
|
||||||
|
const senpaiCard = state.tasks.find(t => t.taskId === senpaiId) |
||||||
|
if (!senpaiCard) { |
||||||
|
errRes.push('invalid member detected') |
||||||
|
return null |
||||||
|
} |
||||||
|
const kohaiCard = state.tasks.find(t => t.taskId === kohaiId) |
||||||
|
if (!kohaiCard) { |
||||||
|
errRes.push('invalid member detected') |
||||||
|
return null |
||||||
|
} |
||||||
|
|
||||||
|
const senpaiVouches = countVouches(senpaiId, state) |
||||||
|
|
||||||
|
let kohaiVouchCards = state.tasks.filter( |
||||||
|
t => kohaiCard.deck.indexOf(t.taskId) >= 0 |
||||||
|
) |
||||||
|
|
||||||
|
let kohaiVouches = kohaiVouchCards.length |
||||||
|
|
||||||
|
kohaiVouchCards.forEach(card => { |
||||||
|
if (card.taskId !== senpaiCard.taskId) { |
||||||
|
kohaiVouches = Math.max(kohaiVouches, countVouches(card.taskId, state)) |
||||||
|
} |
||||||
|
}) |
||||||
|
if (senpaiVouches > kohaiVouches) { |
||||||
|
return 1 |
||||||
|
} else if (kohaiVouches > senpaiVouches) { |
||||||
|
return -1 |
||||||
|
} |
||||||
|
|
||||||
|
errRes.push('member does not have more vouches than other member') |
||||||
|
return 0 |
||||||
|
} |
||||||
|
|
||||||
|
// Returns true if the senpaiId member is both isAheadOf and isDecidedlyMorePopularThan the kohaiId member
|
||||||
|
export function isSenpaiOf(senpaiId, kohaiId, state, errRes?) { |
||||||
|
if (errRes === undefined) { |
||||||
|
errRes = [] |
||||||
|
} |
||||||
|
|
||||||
|
const rank = isAheadOf(senpaiId, kohaiId, state, errRes) |
||||||
|
const vouches = isDecidedlyMorePopularThan(senpaiId, kohaiId, state, errRes) |
||||||
|
if (rank === 1 && vouches === 1) { |
||||||
|
return 1 |
||||||
|
} else if (rank === -1 && vouches === -1) { |
||||||
|
return -1 |
||||||
|
} |
||||||
|
|
||||||
|
errRes.push('member is not a senpai of the other member') |
||||||
|
return 0 |
||||||
|
} |
@ -0,0 +1,132 @@ |
|||||||
|
const defaultSemantics = { |
||||||
|
glossary: { |
||||||
|
card: 'card', |
||||||
|
user: 'member', |
||||||
|
username: 'hackername', |
||||||
|
proposal: 'proposition', |
||||||
|
avatar: 'avatar', |
||||||
|
}, |
||||||
|
levels: { |
||||||
|
0: 'guest', |
||||||
|
1: 'member', |
||||||
|
2: 'elite member', |
||||||
|
}, |
||||||
|
} |
||||||
|
|
||||||
|
let loadedGlossary = {} |
||||||
|
/* Too complex for .env, need a new solution, maybe use fs here to import custom JSON or YAML file |
||||||
|
if (config.semantics && config.semantics.glossary) { |
||||||
|
loadedGlossary = config.semantics.glossary |
||||||
|
}*/ |
||||||
|
const serverGlossary = { ...defaultSemantics.glossary, ...loadedGlossary } |
||||||
|
|
||||||
|
function pluralize(word) { |
||||||
|
let plural = word |
||||||
|
if (Array.isArray(plural)) { |
||||||
|
plural = plural[1] |
||||||
|
} else { |
||||||
|
if (plural[plural.length - 1] === 's') { |
||||||
|
plural = plural + 'es' |
||||||
|
} else { |
||||||
|
plural = plural + 's' |
||||||
|
} |
||||||
|
} |
||||||
|
return plural |
||||||
|
} |
||||||
|
|
||||||
|
export function capitalize(word) { |
||||||
|
if (word.length < 1) { |
||||||
|
return '' |
||||||
|
} |
||||||
|
return word[0].toUpperCase() + word.substring(1) |
||||||
|
} |
||||||
|
|
||||||
|
// Returns the given word or string, with all instances of words in the glossary in configuration.js replaced with their gloss.
|
||||||
|
// In a multi-word string, it will correctly distinguish between all-lowercase keywords and those
|
||||||
|
// with their first letter capitalized, and replace them correctly. Original hardcoded keywords must be typed (in this codebase)
|
||||||
|
// in either all lowercase or with the first letter capitalized to be caught be the word replacement.
|
||||||
|
export function gloss(wordOrSentence, plural = false) { |
||||||
|
let result |
||||||
|
if (wordOrSentence.indexOf(' ') < 0) { |
||||||
|
const word = wordOrSentence |
||||||
|
|
||||||
|
result = word |
||||||
|
const lowercase = word.toLowerCase() |
||||||
|
const pluralEntry = Object.entries(serverGlossary).find( |
||||||
|
([keyword, synonym]) => { |
||||||
|
return ( |
||||||
|
(Array.isArray(keyword) && keyword[1] === lowercase) || |
||||||
|
pluralize(keyword) === lowercase |
||||||
|
) |
||||||
|
} |
||||||
|
) |
||||||
|
const singularEntry = Object.entries(serverGlossary).find( |
||||||
|
([keyword, synonym]) => |
||||||
|
(Array.isArray(keyword) && keyword[0] === lowercase) || |
||||||
|
keyword === lowercase |
||||||
|
) |
||||||
|
if (pluralEntry || singularEntry) { |
||||||
|
result = pluralEntry ? pluralize(pluralEntry[1]) : singularEntry[1] |
||||||
|
if (Array.isArray(result)) { |
||||||
|
result = result[0] |
||||||
|
} |
||||||
|
if (word[0].toLowerCase() !== word[0]) { |
||||||
|
result = result[0].toUpperCase() + result.substring(1) |
||||||
|
} |
||||||
|
} |
||||||
|
} else { |
||||||
|
result = wordOrSentence |
||||||
|
|
||||||
|
Object.entries(serverGlossary).forEach(([keyword, synonym]) => { |
||||||
|
// replace lowercase plural version of the keyword
|
||||||
|
const pluralKeyword = pluralize(keyword) |
||||||
|
const pluralSynonym = pluralize(synonym) |
||||||
|
let regexp = new RegExp('\\b' + pluralKeyword + '\\b', 'g') |
||||||
|
result = result.replace(regexp, pluralSynonym) |
||||||
|
|
||||||
|
// replace capitalized plural version of the keyword
|
||||||
|
const pluralKeywordUppercase = capitalize(pluralKeyword) |
||||||
|
const pluralSynonymUppercase = capitalize(pluralSynonym) |
||||||
|
regexp = new RegExp('\\b' + pluralKeywordUppercase + '\\b', 'g') |
||||||
|
result = result.replace(regexp, pluralSynonymUppercase) |
||||||
|
|
||||||
|
// replace lowercase singular version of the keyword
|
||||||
|
regexp = new RegExp('\\b' + keyword + '\\b', 'g') |
||||||
|
const singularSynonym = Array.isArray(synonym) ? synonym[0] : synonym |
||||||
|
result = result.replace(regexp, singularSynonym) |
||||||
|
|
||||||
|
// replace capitalized singular version of the keyword
|
||||||
|
const singularKeywordUppercase = capitalize(keyword) |
||||||
|
const singularSynonymUppercase = capitalize(singularSynonym) |
||||||
|
regexp = new RegExp('\\b' + singularKeywordUppercase + '\\b', 'g') |
||||||
|
result = result.replace(regexp, singularSynonymUppercase) |
||||||
|
}) |
||||||
|
} |
||||||
|
return result |
||||||
|
} |
||||||
|
|
||||||
|
let loadedLevels = {} |
||||||
|
/*if (config.semantics && config.semantics.levels) { |
||||||
|
loadedLevels = config.semantics.levels |
||||||
|
}*/ |
||||||
|
const serverLevels = { ...defaultSemantics.levels, ...loadedLevels } |
||||||
|
|
||||||
|
export function glossLevel(level) { |
||||||
|
if (level < 0) { |
||||||
|
return null |
||||||
|
} |
||||||
|
let highestMatchingWord |
||||||
|
Object.entries(serverLevels).some(([index, word]) => { |
||||||
|
if (index <= level) { |
||||||
|
highestMatchingWord = word |
||||||
|
} |
||||||
|
if (index >= level) { |
||||||
|
return true |
||||||
|
} |
||||||
|
}) |
||||||
|
return highestMatchingWord || 'member' |
||||||
|
} |
||||||
|
|
||||||
|
export function getSemantics() { |
||||||
|
return { glossary: serverGlossary, levels: serverLevels } |
||||||
|
} |
@ -0,0 +1,351 @@ |
|||||||
|
// Every card has a color, right now only five preset colors exist (they can be renamed with the glossary and recolored with CSS)
|
||||||
|
export type Color = 'red' | 'yellow' | 'green' | 'purple' | 'blue' | 'black' |
||||||
|
|
||||||
|
// The regions or areas within a card
|
||||||
|
// There are four main zones within a card: the priorities,
|
||||||
|
// the optional pinboard (grid/pyramid/rune),the subTasks (main pile), and completed cards
|
||||||
|
export type CardZone = |
||||||
|
| 'card' // The card itself, the whole card
|
||||||
|
| 'priorities' // The card's priorities section (right card drawer)
|
||||||
|
| 'grid' // A pinboard that can be added to the card (shows on card)
|
||||||
|
| 'subTasks' // The main pile of subcards within this card (bottom card drawer)
|
||||||
|
| 'completed' // Checked-off tasks archived when discarded (viewable in priorities card drawer)
|
||||||
|
| 'context' // The context area above a card that contains the card history (cards can be dragged from here)
|
||||||
|
| 'discard' // The background of the page behind the card, where cards can be dropped to discard
|
||||||
|
| 'panel' // Any other unspecified side panel where cards can be dragged out of (to copy, not move, the card)
|
||||||
|
| 'gifts' // The gifts area at the top left of the member card where cards you receive accumulate.
|
||||||
|
| 'stash' // The stashed cards area of the card (right card drawer)
|
||||||
|
|
||||||
|
// A card's pinboard can be in one of three styles
|
||||||
|
export type PinboardStyle = 'grid' | 'pyramid' | 'rune' |
||||||
|
|
||||||
|
// The global left sidebar can open and display one of these tabs at a time
|
||||||
|
export type LeftSidebarTab = |
||||||
|
| 'hub' |
||||||
|
| 'gifts' |
||||||
|
| 'guilds' |
||||||
|
| 'members' |
||||||
|
| 'calendar' |
||||||
|
| 'bounties' |
||||||
|
| 'manual' |
||||||
|
| 'search' |
||||||
|
| 'deck' |
||||||
|
|
||||||
|
// The global right sidebar can display one of these Bull tabs
|
||||||
|
export type RightSidebarTab = 'resources' | 'p2p' | 'crypto' | 'membership' |
||||||
|
|
||||||
|
// The right side of a card displays these tabs, which can be clicked to open the corresponding card drawer
|
||||||
|
export type CardTab = 'priorities' | 'timecube' | 'lightning' |
||||||
|
|
||||||
|
// When a member gifts/sends/passes a card to another member, the cards .pass array holds an array of passes
|
||||||
|
// The 0th element holds the memberId of the sender, and the 1st element holds the memberId of the recipient
|
||||||
|
export type CardPass = string[2] |
||||||
|
|
||||||
|
// Definition of an AO
|
||||||
|
export interface AoState { |
||||||
|
session: string |
||||||
|
token: string |
||||||
|
loggedIn: boolean |
||||||
|
user: string |
||||||
|
ao: ConnectedAo[] |
||||||
|
sessions: Session[] |
||||||
|
members: Member[] |
||||||
|
tasks: Task[] |
||||||
|
resources: Resource[] |
||||||
|
memes: Meme[] |
||||||
|
socketState?: string |
||||||
|
protectedRouteRedirectPath?: string |
||||||
|
bookings?: Booking[] // Used on server to track calendar events on a timer
|
||||||
|
cash: { |
||||||
|
address: string |
||||||
|
alias: string |
||||||
|
currency: string |
||||||
|
spot: number |
||||||
|
rent: number |
||||||
|
cap: number |
||||||
|
quorum: number |
||||||
|
pay_index: number |
||||||
|
usedTxIds: number[] |
||||||
|
outputs: Output[] |
||||||
|
channels: Channel[] |
||||||
|
info: SatInfo |
||||||
|
} |
||||||
|
loader?: { |
||||||
|
token: string |
||||||
|
session: string |
||||||
|
connected: string |
||||||
|
connectionError: string |
||||||
|
reqStatus: string |
||||||
|
lastPing: number |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// An AO serves its members, who each have an account on the AO server.
|
||||||
|
export interface Member { |
||||||
|
type: 'member-created' // The event is added directly to the database so it has this as an artifact, could filter on member-created and remove here
|
||||||
|
name: string // The name of the member
|
||||||
|
memberId: string // The unique UUID of the member
|
||||||
|
address: string // ???
|
||||||
|
active: number // The member's active status. Number increases each consecutive active month.
|
||||||
|
balance: number // Member's point balance
|
||||||
|
badges: [] // Badges that the member has collected
|
||||||
|
tickers: Ticker[] // Customizable list of crypto tickers on the member's right sidebar
|
||||||
|
info: {} // ???
|
||||||
|
timestamp: number // When the member was created
|
||||||
|
lastUsed: number // Last time the member logged in, used a resource, or barked
|
||||||
|
muted: boolean // Whether the member has sound effects turned on or off (sound effects not currently implemented)
|
||||||
|
priorityMode: boolean // Whether the member has activated Priority Mode, which shows the first priority above its parent card
|
||||||
|
fob: string // The member can enter a fob ID number from a physical fob, saved here for when they tap
|
||||||
|
potentials: Signature[] // List of potential actions built up on the member (not currently in use)
|
||||||
|
banned: boolean // True if the member is currently banned (member continues to exist)
|
||||||
|
draft: string // The member's currently-saved draft (also saved on client)
|
||||||
|
tutorial?: boolean // Whether the member has completed the initial interactive tour of the AO
|
||||||
|
p0wned?: boolean // Whether the member has had their password reset (changes to false when they set it themselves)
|
||||||
|
phone?: string // Phone number used for Signal notifications
|
||||||
|
} |
||||||
|
|
||||||
|
// A member can create and collect cards. The words 'card' and 'task' are used as synonyms, because the AO is meant for action.
|
||||||
|
export interface Task { |
||||||
|
taskId: string // ao-react: Random UUID | ao-3: CRC-32 hash of the content
|
||||||
|
name: string // The text of the card, the main content. Can be plain text, Markdown, or HTML and JavaScript (optimized for injection).
|
||||||
|
color: Color // Color of the card as a word | Future: Could be any color word or hex code paired with a word naming the color
|
||||||
|
deck: string[] // *Array of memberIds of members who grabbed and are holding the card in their deck
|
||||||
|
guild: string | boolean // Optional guild / pin / tag title for the card. This is editable (unlike cards currently). Guild cards are indexed in Guilds sidebar on left. (Value of 'true' could mean that guild name equals card text [not implemented yet].)
|
||||||
|
address: string // Mainnet bitcoin address for this card (generated by calling address)
|
||||||
|
bolt11?: string // Lightning network bitcoin address for this carde (generated by calling invoice-created)
|
||||||
|
payment_hash: string //
|
||||||
|
book?: Booking // Book/schedule this card as an event on the calendar
|
||||||
|
priorities: string[] // *Array of taskIds of cards prioritized within this card
|
||||||
|
subTasks: string[] // *Array of taskIds of cards within this card
|
||||||
|
completed: string[] // *Array of taskIds of checked-off completed cards within this cards. Cards saved here when discarded with any checkmarks on them.
|
||||||
|
pinboard?: Pinboard | null // *Pinboard object containing pinboard properties
|
||||||
|
pins?: Pin[] // *New way of doing the Grid, Pyramid, and upcoming Rune layouts for the card
|
||||||
|
parents: string[] // *List of this cards parents, ought to be kept updated by mutations.
|
||||||
|
claimed: string[] // Lists of taskIds who have checked this card (thus claiming the bounty)
|
||||||
|
claimInterval?: number // Automatic uncheck timer in milliseconds [this feature will change to uncheck the cards within]
|
||||||
|
//uncheckInterval // Rename of claimInterval to be rolled out
|
||||||
|
uncheckThisCard?: boolean // Unchecks this card every uncheckInterval if true
|
||||||
|
uncheckPriorities?: boolean // Unchecks prioritized cards every uncheckInterval if true
|
||||||
|
uncheckPinned?: boolean // Unchecks pinned cards every uncheckInterval if true (maybe could combine with uncheckPriorities)
|
||||||
|
dimChecked?: boolean // If true, checked cards on the pinboard and in the priorities list will display visually dimmed to make tasking easier
|
||||||
|
signed: Signature[] // Members can explicitly sign cards to endorse them (future option to counter-sign as -1 is already built-in)
|
||||||
|
passed: string[][] // Array of [senderMemberId, receiverMemberId] pairs of pending gifts sent to the receiving member. Cleared when opened.
|
||||||
|
giftCount?: number // Count of unopened gift cards that ought to be kept automatically updated, for showing this number ot other members
|
||||||
|
lastClaimed: number // The last time someone checked this card off (Unix timestamp)
|
||||||
|
allocations: Allocation[] // List of points temporarily allocated to this card from parent cards, making this card a claimable bounty
|
||||||
|
boost: number // Bonus points on the card (?)
|
||||||
|
goal?: number // Optional points goal shows after the current number of points on a card, e.g., 8/10 points raised in the crowdfund.
|
||||||
|
highlights: number[] |
||||||
|
seen: UserSeen[] // Array of events marking the first (?) or most recent (?) time they looked at the card. Used for unread markers.
|
||||||
|
timelog?: LabourTime[] // Arary of timelog events on the card
|
||||||
|
created: number // When the card was created (Unix timestamp)
|
||||||
|
showChatroom?: boolean // Whether or not to show the chatroom tab. Only allowed on cards with a .guild set for simplicity and transparency's sake.
|
||||||
|
avatars?: AvatarLocation[] // When a member joins a chatroom, it shows they are "at" that card. | Future: Little avator icons that can be moved from card to card or clicked to follow.
|
||||||
|
memberships?: Membership[] // Members can "join" a card as members. The first member is automatically Level 2 and can boss the Level 1's around. You can decrease your level and lose your power.
|
||||||
|
showStash?: boolean // Whether or not to show the stash tab. Only allowed on cards with a .guild set for simplicity and transparency's sake.
|
||||||
|
stash?: { |
||||||
|
// *Stash of more cards associated with this card. Members have access to stashes of their level and below.
|
||||||
|
[key: number]: string[] // Each numbered stash level contains a list of taskIds of cards stored in that stash level.
|
||||||
|
} |
||||||
|
unionHours?: number // Number of estimated hours for the task
|
||||||
|
unionSkill?: number // Skill level required for the task (0-5)
|
||||||
|
unionHazard?: number // Hazard level for the task (0-5)
|
||||||
|
loadedFromServer?: boolean // True if the card has been loaded from the server, false if empty placeholder taskId object
|
||||||
|
stars?: number // Can be simple number or later a Rating[]
|
||||||
|
touches?: number // New feature to count number of time a card was handled, to identify popular cards and personal hotspots.
|
||||||
|
aoGridToolDoNotUpdateUI?: boolean // Rendering hack, maybe this can be improved and removed
|
||||||
|
// *These properties contain taskIds of cards that are within or closely associated with this card, for the purposes of search, content buffering, etc.
|
||||||
|
} |
||||||
|
|
||||||
|
// A booked/scheduled event or resource
|
||||||
|
export interface Booking { |
||||||
|
memberId: string // The member that scheduled the event (?)
|
||||||
|
startTs: number // The start of the event (Unix timestamp)
|
||||||
|
endTs: number // The end time of the event. Optional—but if omitted behavior is undefined. (Unix timestamp)
|
||||||
|
} |
||||||
|
|
||||||
|
// An AO can connect to another AO over tor to send cards
|
||||||
|
export interface ConnectedAo { |
||||||
|
name?: string |
||||||
|
address: string |
||||||
|
outboundSecret: false | string |
||||||
|
inboundSecret: string |
||||||
|
lastContact: number |
||||||
|
links: string[] |
||||||
|
} |
||||||
|
|
||||||
|
// Hardware devices can be connected to the AO as resources over LAN. Resources can be activated in the Bull (right sidebar).
|
||||||
|
export interface Resource { |
||||||
|
resourceId: string // UUID of the resource
|
||||||
|
name: string |
||||||
|
charged: number // How many points it costs to use the resource each time
|
||||||
|
secret: string // ???
|
||||||
|
trackStock: boolean // If true, the resource will track its inventory
|
||||||
|
stock: number // Simple numeric tracking of inventory stock, works for most things
|
||||||
|
} |
||||||
|
|
||||||
|
// Files detected in the ~/.ao/memes folder are each loaded as a Meme
|
||||||
|
export interface Meme { |
||||||
|
memeId: string // UUID that matches the corresponding taskId of the card that is created in lockstep with the Meme object.
|
||||||
|
filename: string // Just the filename and extension, not the path
|
||||||
|
hash: string // Hash of the file
|
||||||
|
filetype: string |
||||||
|
} |
||||||
|
|
||||||
|
// Cordinates of a card on a pinboard or in a list of cards
|
||||||
|
export interface Coords { |
||||||
|
x?: number |
||||||
|
y: number |
||||||
|
} |
||||||
|
|
||||||
|
// Specifies a card taskId at a given location at a specific location within another card.
|
||||||
|
// For pinboard locations, .coords must coordinates for the current pinboard type
|
||||||
|
// For stashed cards, .level specifies which stash level the card is stored in
|
||||||
|
export interface CardLocation { |
||||||
|
taskId?: string // Optional because sometimes a CardLocation is used to described a location where a card will be played/placed
|
||||||
|
inId?: string |
||||||
|
zone?: CardZone // Optional because sometimes a CardLocation describes a card that is not in any location yet
|
||||||
|
level?: number |
||||||
|
coords?: Coords |
||||||
|
} |
||||||
|
|
||||||
|
// An atomic card play, defining a card to remove (from) and a card to place (to)
|
||||||
|
export interface CardPlay { |
||||||
|
from: CardLocation |
||||||
|
to: CardLocation |
||||||
|
} |
||||||
|
|
||||||
|
// Defines the dimensions and other properties of a spread or layout of cards.
|
||||||
|
// Could be expanded or inherited from to create new types of spreads such as a freeform canvas or non-euclidian pinboard.
|
||||||
|
export interface Pinboard { |
||||||
|
spread: PinboardStyle |
||||||
|
height: number |
||||||
|
width?: number |
||||||
|
size: number // Size of squares, roughly in ems
|
||||||
|
} |
||||||
|
|
||||||
|
// A card pinned to a pinboard
|
||||||
|
// Pinboards set to rune layout only use x (y always 0)
|
||||||
|
export interface Pin { |
||||||
|
taskId: string |
||||||
|
y: number |
||||||
|
x?: number |
||||||
|
} |
||||||
|
|
||||||
|
// A guild can temporarily allocate some of its points to a task in its priorities
|
||||||
|
// Anyone who checks off the task will claim the points on it, including all its allocations
|
||||||
|
// Allocated points are actually moved from the parent card at the time (in state) and moved back if the card is deprioritized
|
||||||
|
// All mutations related to moving points around must be perfect to prevent double spend issues
|
||||||
|
export interface Allocation { |
||||||
|
type?: string |
||||||
|
taskId: string |
||||||
|
allocatedId: string |
||||||
|
amount: number |
||||||
|
blame?: string |
||||||
|
} |
||||||
|
|
||||||
|
// A member may sign a card with a positive (1, default), neutral (0), or opposing (-1) opinion, or instead sign with a note/comment
|
||||||
|
// This can be used for votes, upvotes/downvotes (and maybe reaction emojis? reactions could replace signing)
|
||||||
|
export interface Signature { |
||||||
|
memberId: string |
||||||
|
timestamp: Date |
||||||
|
opinion: number | string |
||||||
|
} |
||||||
|
|
||||||
|
// A member may rate a card 0-5 stars
|
||||||
|
export interface Rating { |
||||||
|
memberId: string |
||||||
|
stars: number |
||||||
|
} |
||||||
|
|
||||||
|
// A card is marked when a member sees it for the first time. Used to mark unread cards (feature currently inactive).
|
||||||
|
export interface UserSeen { |
||||||
|
memberId: string |
||||||
|
timestamp: Date |
||||||
|
} |
||||||
|
|
||||||
|
// Log of one duration of time to the timelog on a card
|
||||||
|
export interface LabourTime { |
||||||
|
memberId: string |
||||||
|
taskId: string |
||||||
|
inId: string |
||||||
|
start: number |
||||||
|
stop: number |
||||||
|
} |
||||||
|
|
||||||
|
// Each member has an "avatar" (currently no icon yet) that can be placed on a card to show your location to other members.
|
||||||
|
// When you join a chatroom on a card, you automatically hop your avatar to that card (task-visited event).
|
||||||
|
export interface AvatarLocation { |
||||||
|
memberId: string |
||||||
|
timestamp: number |
||||||
|
area: number |
||||||
|
} |
||||||
|
|
||||||
|
// Members can join guilds. Members of a guild have a numeric level.
|
||||||
|
// Each level has a stash of cards that only members of that level or above can edit.
|
||||||
|
// Members of higher levels can change the level of members with the same or lower levels.
|
||||||
|
// The system is stupid and you can increase your own level too low and lose your power, or too high and mess up the stash.
|
||||||
|
export interface Membership { |
||||||
|
memberId: string |
||||||
|
level: number |
||||||
|
} |
||||||
|
|
||||||
|
// Members can add tickers in the sidebar, which show the exchange rate between a 'from' and 'to' currency (three-letter currency abbreviation)
|
||||||
|
export interface Ticker { |
||||||
|
from: string |
||||||
|
to: string |
||||||
|
} |
||||||
|
|
||||||
|
export interface Output { |
||||||
|
value: number |
||||||
|
} |
||||||
|
|
||||||
|
export interface Channel { |
||||||
|
channel_sat: number |
||||||
|
channel_total_sat: number |
||||||
|
} |
||||||
|
|
||||||
|
// A browser session object
|
||||||
|
export interface Session { |
||||||
|
type: 'session-created' // Event is added directly to the database, this is an artifact of that
|
||||||
|
session: string // Session string?
|
||||||
|
ownerId: string // MemeberId of the session owner
|
||||||
|
timestamp: Date // When the session was created
|
||||||
|
} |
||||||
|
|
||||||
|
export interface LightningChannel { |
||||||
|
peer_id?: any |
||||||
|
funding_txid?: any |
||||||
|
state?: any |
||||||
|
connected?: boolean |
||||||
|
channel_total_sat: number |
||||||
|
channel_sat: number |
||||||
|
} |
||||||
|
|
||||||
|
export interface SatInfo { |
||||||
|
channels?: LightningChannel[] |
||||||
|
mempool?: { sampleTxns: any[]; size: any; bytes: any } |
||||||
|
blockheight?: number |
||||||
|
blockfo?: any |
||||||
|
id?: any |
||||||
|
outputs?: any[] |
||||||
|
address?: { address: string }[] |
||||||
|
} |
||||||
|
|
||||||
|
export interface SearchResults { |
||||||
|
query: string |
||||||
|
page: number |
||||||
|
missions: Task[] |
||||||
|
members: Task[] |
||||||
|
tasks: Task[] |
||||||
|
all: Task[] |
||||||
|
length: number |
||||||
|
} |
||||||
|
|
||||||
|
export const emptySearchResults = { |
||||||
|
missions: [], |
||||||
|
members: [], |
||||||
|
tasks: [], |
||||||
|
all: [], |
||||||
|
length: 0, |
||||||
|
} |
@ -0,0 +1,44 @@ |
|||||||
|
export const cancelablePromise = promise => { |
||||||
|
let isCanceled = false |
||||||
|
|
||||||
|
const wrappedPromise = new Promise((resolve, reject) => { |
||||||
|
promise.then( |
||||||
|
value => (isCanceled ? reject({ isCanceled, value }) : resolve(value)), |
||||||
|
error => reject({ isCanceled, error }) |
||||||
|
) |
||||||
|
}) |
||||||
|
|
||||||
|
return { |
||||||
|
promise: wrappedPromise, |
||||||
|
cancel: () => (isCanceled = true), |
||||||
|
} |
||||||
|
} |
||||||
|
export const noop = () => {} |
||||||
|
|
||||||
|
export const delay = n => new Promise(resolve => setTimeout(resolve, n)) |
||||||
|
|
||||||
|
export const isObject = obj => { |
||||||
|
return Object.prototype.toString.call(obj) === '[object Object]' |
||||||
|
} |
||||||
|
|
||||||
|
export const convertToDuration = (milliseconds: number) => { |
||||||
|
const stringifyTime = (time: number): string => String(time).padStart(2, '0') |
||||||
|
const seconds = Math.floor(milliseconds / 1000) |
||||||
|
const minutes = Math.floor(seconds / 60) |
||||||
|
const hours = Math.floor(minutes / 60) |
||||||
|
return `${stringifyTime(hours)}:${stringifyTime( |
||||||
|
minutes % 60 |
||||||
|
)}:${stringifyTime(seconds % 60)}` |
||||||
|
} |
||||||
|
|
||||||
|
export const convertToTimeWorked = (milliseconds: number) => { |
||||||
|
const seconds = Math.floor(milliseconds / 1000) |
||||||
|
const minutes = Math.floor(seconds / 60) |
||||||
|
const hours = Math.floor(minutes / 60) |
||||||
|
|
||||||
|
if (hours > 0) { |
||||||
|
return `${hours}h, ${minutes % 60}m` |
||||||
|
} else { |
||||||
|
return `${minutes % 60}m` |
||||||
|
} |
||||||
|
} |
Loading…
Reference in new issue