You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
798 lines
24 KiB
798 lines
24 KiB
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 |
|
}
|
|
|