src/hooks/useTrackingState.ts

Summary

Maintainability
C
7 hrs
Test Coverage
F
18%
import { useCallback } from 'react'
import { format } from 'date-fns-tz'
import { atom, selector, useRecoilState, useRecoilValue } from 'recoil'

import { nodeState, useTaskManager } from '@/hooks/useTaskManager'
import { taskRecordKeyState } from '@/hooks/useTaskRecordKey'
import { updateRecords, saveRecords, loadRecords } from '@/hooks/useTaskStorage'
import {
  useActivity,
  appendActivities,
  getActivities,
} from '@/hooks/useActivity'
import { useAlarms } from '@/hooks/useAlarms'
import { STORAGE_KEY, Storage } from '@/services/storage'
import { Ipc } from '@/services/ipc'
import { moveLine } from '@/services/util'
import Log from '@/services/log'
import { CalendarEvent } from '@/services/google/calendar'
import { TrackingState } from '@/@types/global'
import { Node, setNodeByLine } from '@/models/node'
import { Task } from '@/models/task'
import { Time } from '@/models/time'
import { TaskRecordKey } from '@/models/taskRecordKey'

const TIME_FORMAT = "yyyy-MM-dd'T'HH:mm:ssXXX"

export const stopTrackings = (
  root: Node,
  trackings: TrackingState[],
): [Node, CalendarEvent[]] => {
  const events = []
  for (const tracking of trackings) {
    if (!tracking.isTracking) {
      continue
    }
    const node = root.find((n) => n.line === tracking.line)
    if (node == null || !(node.data instanceof Task)) {
      continue
    }

    // Clone the objects for updating elapsed time.
    const newNode = node.clone()
    const newTask = newNode.data as Task
    newTask.trackingStop(tracking.trackingStartTime)
    root = setNodeByLine(root, node.line, newNode)

    // For updating activities.
    const start = format(tracking.trackingStartTime, TIME_FORMAT)
    const end = format(Date.now(), TIME_FORMAT)
    const ems = Date.now() - tracking.trackingStartTime
    const time = Time.parseMs(ems)
    events.push({
      id: '' + Math.random(),
      title: newTask.title,
      time,
      start,
      end,
    })
  }

  return [root, events]
}

export const saveStates = async (
  key: TaskRecordKey,
  root: Node,
  trackings: TrackingState[],
  events: CalendarEvent[],
) => {
  // Update root Node.
  const records = await loadRecords()
  const newRecords = updateRecords(records, key, root)
  await saveRecords(newRecords)

  // Update tracking state.
  await Storage.set(STORAGE_KEY.TRACKING_STATE, trackings)

  // Update activities.
  const activities = await getActivities()
  if (events.length > 0) appendActivities(activities, events)
}

const trackingState = atom<TrackingState[]>({
  key: 'trackingState',
  default: selector({
    key: 'savedTrackingStateList',
    get: async () => {
      const trackings = (await Storage.get(
        STORAGE_KEY.TRACKING_STATE,
      )) as TrackingState[]
      if (!trackings) return []
      return trackings
    },
  }),
  effects: [
    ({ onSet, setSelf }) => {
      Storage.addListener(STORAGE_KEY.TRACKING_STATE, (newVal) => {
        setSelf(newVal)
      })

      onSet((state) => {
        // Automatically save the tracking status.
        void Storage.set(STORAGE_KEY.TRACKING_STATE, state)
      })
    },
  ],
})

const trackingStateSelector = selector<TrackingState[]>({
  key: 'trackingStateSelector',
  get: ({ get }) => {
    const key = get(taskRecordKeyState)
    const trackings = get(trackingState)
    const root = get(nodeState)

    // Since the node id changes with each parsing, find and update the new id
    // using the line number as a key.
    return trackings.map((t) => {
      if (t.key !== key.toKey()) {
        return t
      }
      const node = root.find((n) => n.line === t.line)
      if (node) {
        return {
          ...t,
          nodeId: node.id,
        }
      }
      return t
    })
  },
  set: ({ set }, state) => {
    set(trackingState, state)
  },
})

interface useTrackingStateReturn {
  trackings: TrackingState[]
  startTracking: (node: Node) => void
  stopTracking: (node: Node, checked?: boolean) => void
}

export function useTrackingState(): useTrackingStateReturn {
  const manager = useTaskManager()
  const { appendActivities } = useActivity()
  const { setAlarmsForTask, stopAlarmsForTask } = useAlarms()
  const [trackings, setTrackings] = useRecoilState(trackingStateSelector)
  const trackingKey = useRecoilValue(taskRecordKeyState)

  const startTracking = useCallback(
    (node: Node) => {
      let root = manager.getRoot()

      // start new task.
      const newNode = node.clone()
      const newTask = newNode.data as Task
      const trackingStartTime = newTask.trackingStart()
      const tracking = {
        key: trackingKey.toKey(),
        nodeId: node.id,
        isTracking: true,
        trackingStartTime,
        line: node.line,
      }
      root = setNodeByLine(root, node.line, newNode)

      // Stop other tasks.
      const filtered = trackings.filter((n) => n.nodeId !== node.id)
      const [newRoot, events] = stopTrackings(root, filtered)
      manager.setRoot(newRoot)
      if (events.length > 0) appendActivities(events)

      // Stop previous alarms.
      stopAlarmsForTask()

      setTrackings([tracking])
      Ipc.send({
        command: 'startTracking',
        param: 0,
      })
      setAlarmsForTask(newTask)
    },
    [trackings],
  )

  const stopTracking = useCallback(
    (node: Node, checked?: boolean) => {
      // Clone the objects for updating.
      const newNode = node.clone()
      const newTask = newNode.data as Task

      // update node & task
      const tracking = trackings.find((n) => n.nodeId === node.id)
      if (tracking) newTask.trackingStop(tracking.trackingStartTime)
      if (checked != null) newTask.setComplete(checked)
      manager.setNodeByLine(newNode, node.line)

      // update calendar events
      if (tracking) {
        const start = format(tracking.trackingStartTime, TIME_FORMAT)
        const end = format(Date.now(), TIME_FORMAT)
        const ems = Date.now() - tracking.trackingStartTime
        const time = Time.parseMs(ems)
        appendActivities([
          {
            id: '' + Math.random(),
            title: newTask.title,
            time,
            start,
            end,
          },
        ])
      }

      // stop tracking state
      const newVal = trackings.filter((n) => {
        return n.nodeId !== node.id
      })
      setTrackings(newVal)
      Ipc.send({ command: 'stopTracking' })
      stopAlarmsForTask()
    },
    [trackings],
  )

  return {
    trackings,
    startTracking,
    stopTracking,
  }
}

export function useTrackingMove() {
  const [trackings, setTrackings] = useRecoilState(trackingStateSelector)

  const moveTracking = useCallback(
    (from: number, to: number, length: number) => {
      const newVal = trackings
        .map((n) => {
          return {
            ...n,
            line: moveLine(n.line, from, to, length),
          }
        })
        .filter((n) => n.line != null)
      setTrackings(newVal)
    },
    [trackings],
  )

  return {
    trackings,
    moveTracking,
  }
}

interface useTrackingStopReturn {
  stopAllTracking: () => void
}

export function useTrackingStop(): useTrackingStopReturn {
  const manager = useTaskManager()
  const root = manager.getRoot()
  const trackingKey = useRecoilValue(taskRecordKeyState)
  const [trackings] = useRecoilState(trackingStateSelector)
  const { stopAlarmsForTask } = useAlarms()

  const stopAllTracking = useCallback(() => {
    Log.d('stopAllTracking')
    const [newRoot, events] = stopTrackings(root, trackings)
    manager.setRoot(newRoot)
    saveStates(trackingKey, newRoot, [], events)
    Ipc.send({ command: 'stopTracking' })
    stopAlarmsForTask()
  }, [trackings])

  return { stopAllTracking }
}