このコミットが含まれているのは:
+198
-22
@@ -1,34 +1,160 @@
|
||||
import { apiGet } from '@/lib/api'
|
||||
|
||||
import type { LocaleCode } from '@/lib/locales'
|
||||
import type { PostVersion, TagVersion } from '@/types'
|
||||
import type { TagNameSnapshot } from '@/types'
|
||||
|
||||
export type CountsResource = 'posts' | 'tags'
|
||||
export type CountsName = TagNameSnapshot & {
|
||||
languageCode: string; languageName?: string | null; scriptName?: string | null }
|
||||
export type CountsTag = {
|
||||
id: number
|
||||
kind: 'internal' | 'external'
|
||||
tagId: number | null
|
||||
externalTagId: number | null
|
||||
versionNo: number | null
|
||||
name: string | null
|
||||
category: string | null
|
||||
sections: { beginMs: number; endMs: number | null }[]
|
||||
names: CountsName[] }
|
||||
export type CountsVersion = {
|
||||
id: number
|
||||
resourceId: number
|
||||
versionNo: number
|
||||
isLatest: boolean
|
||||
createdAt: string
|
||||
createdByUser: { id: number; name: string | null } | null
|
||||
title?: string | null
|
||||
url?: string
|
||||
thumbnailBase?: string | null
|
||||
tags?: CountsTag[]
|
||||
parents: { id: number }[]
|
||||
videoMs?: number | null
|
||||
originalCreatedFrom?: string | null
|
||||
originalCreatedBefore?: string | null
|
||||
name?: string | null
|
||||
names?: CountsName[]
|
||||
category?: string
|
||||
aliases?: { languageCode: string; name: string; scriptCode: string }[]
|
||||
deprecated?: string | null }
|
||||
export type CountsResult = {
|
||||
count: number
|
||||
page: number
|
||||
limit: number
|
||||
groupCount?: number
|
||||
groups?: { key: string | number | null; name: string | null; count: number }[]
|
||||
versions?: (PostVersion | TagVersion)[] }
|
||||
versions?: CountsVersion[] }
|
||||
export type CountSort = { key: string; direction: 'asc' | 'desc' }
|
||||
export type CountField = { key: string; label: string; children?: CountField[] }
|
||||
export type CountRecord = Record<string, unknown>
|
||||
|
||||
export const countColumns = {
|
||||
created_at: 'counts:columns.changedAt', resource_id: 'counts:columns.resourceId',
|
||||
version_no: 'history:version', user: 'history:actor',
|
||||
thumbnail: 'counts:columns.thumbnailDisplay', resource_id: 'counts:columns.resourceId',
|
||||
version_no: 'history:version', is_latest: 'counts:columns.latest',
|
||||
title: 'counts:columns.postTitle', url: 'counts:columns.postURL',
|
||||
tags: 'counts:columns.tags', added_tags: 'counts:columns.addedTags',
|
||||
removed_tags: 'counts:columns.removedTags', name: 'history:tagName',
|
||||
category: 'common:categoryLabel', aliases: 'tags:aliases', parents: 'tags:parents',
|
||||
deprecated: 'counts:columns.deprecated' }
|
||||
|
||||
thumbnail_base: 'counts:columns.thumbnailURL', tags: 'counts:columns.tags',
|
||||
parents: 'shared:labels.parentPosts', video_ms: 'shared:labels.duration',
|
||||
original_created_at: 'shared:labels.originalCreatedAt',
|
||||
original_created_from: 'counts:columns.originalFrom',
|
||||
original_created_before: 'counts:columns.originalBefore',
|
||||
name: 'history:tagName', names: 'counts:columns.names',
|
||||
category: 'common:categoryLabel', aliases: 'tags:aliases',
|
||||
deprecated: 'counts:columns.deprecated', user: 'history:actor',
|
||||
created_at: 'counts:columns.changedAt' }
|
||||
export type CountColumn = keyof typeof countColumns
|
||||
|
||||
const aliasFields: CountField[] = [
|
||||
{ key: 'name', label: 'history:name' },
|
||||
{ key: 'scriptCode', label: 'history:nameChanges.script' }]
|
||||
const nameFields: CountField[] = [
|
||||
{ key: 'languageCode', label: 'counts:columns.language' },
|
||||
{ key: 'scriptCode', label: 'history:nameChanges.script' },
|
||||
{ key: 'name', label: 'history:name' },
|
||||
{ key: 'aliases', label: 'tags:aliases', children: aliasFields },
|
||||
{ key: 'autoGenerated', label: 'history:nameChanges.automatic' }]
|
||||
const nested: Partial<Record<CountColumn, CountField[]>> = {
|
||||
tags: [
|
||||
{ key: 'id', label: 'counts:columns.resourceId' },
|
||||
{ key: 'versionNo', label: 'history:version' },
|
||||
{ key: 'kind', label: 'counts:columns.kind' },
|
||||
{ key: 'name', label: 'history:name' },
|
||||
{ key: 'category', label: 'common:categoryLabel' },
|
||||
{ key: 'sections', label: 'counts:columns.sections', children: [
|
||||
{ key: 'beginMs', label: 'counts:columns.begin' },
|
||||
{ key: 'endMs', label: 'counts:columns.end' }] },
|
||||
{ key: 'names', label: 'counts:columns.names', children: nameFields }],
|
||||
names: nameFields,
|
||||
aliases: [{ key: 'languageCode', label: 'counts:columns.language' }, ...aliasFields],
|
||||
parents: [{ key: 'id', label: 'counts:columns.resourceId' }] }
|
||||
|
||||
|
||||
export const columnsForCounts = (resource: CountsResource): CountColumn[] => [
|
||||
'created_at', 'resource_id', 'version_no', 'user',
|
||||
...(resource === 'posts' ? ['thumbnail'] as const : []),
|
||||
'resource_id', 'version_no', 'is_latest',
|
||||
...(resource === 'posts'
|
||||
? ['title', 'url', 'tags', 'added_tags', 'removed_tags'] as const
|
||||
: ['name', 'category', 'aliases', 'parents', 'deprecated'] as const)]
|
||||
? ['title', 'url', 'thumbnail_base', 'tags', 'parents', 'video_ms',
|
||||
'original_created_at', 'original_created_from', 'original_created_before'] as const
|
||||
: ['name', 'names', 'category', 'aliases', 'parents', 'deprecated'] as const),
|
||||
'user', 'created_at']
|
||||
|
||||
|
||||
export const fieldsForCounts = (resource: CountsResource): CountField[] =>
|
||||
columnsForCounts (resource).map (key => ({
|
||||
key,
|
||||
label: (key === 'parents' && resource === 'tags') ? 'tags:parents' : countColumns[key],
|
||||
children: nested[key] }))
|
||||
|
||||
|
||||
export const selectedCountColumns = (
|
||||
resource: CountsResource,
|
||||
params: URLSearchParams,
|
||||
): CountColumn[] => {
|
||||
const selected = params.has ('columns') ? params.get ('columns')!.split (',') :
|
||||
columnsForCounts (resource).filter (key => ![
|
||||
'thumbnail', 'original_created_from', 'original_created_before']
|
||||
.includes (key))
|
||||
return columnsForCounts (resource).filter (key => selected.includes (key))
|
||||
}
|
||||
|
||||
|
||||
export const countChildPaths = (fields: CountField[], prefix = ''): string[] =>
|
||||
fields.flatMap (field => {
|
||||
const path = prefix ? `${prefix}.${field.key}` : field.key
|
||||
return [path, ...countChildPaths (field.children ?? [], path)]
|
||||
})
|
||||
|
||||
|
||||
export const selectedCountChildren = (params: URLSearchParams): Set<string> =>
|
||||
new Set (params.has ('children') ? params.get ('children')!.split (',') :
|
||||
Object.entries (nested).flatMap (([key, fields]) =>
|
||||
countChildPaths (fields, key)))
|
||||
|
||||
|
||||
export const readCountSort = (value: string | null): CountSort[] =>
|
||||
(value ?? '').split (',').flatMap (entry => {
|
||||
const [key, direction] = entry.split (':')
|
||||
return (key && (direction === 'asc' || direction === 'desc'))
|
||||
? [{ key, direction }] : []
|
||||
})
|
||||
|
||||
|
||||
export const writeCountSort = (sort: CountSort[]): string =>
|
||||
sort.map (item => `${item.key}:${item.direction}`).join (',')
|
||||
|
||||
|
||||
export const changeCountSort = (
|
||||
sort: CountSort[],
|
||||
key: string,
|
||||
additive: boolean,
|
||||
): CountSort[] => {
|
||||
const previous = sort.find (item => item.key === key)
|
||||
if (!additive)
|
||||
return [{ key, direction: previous?.direction === 'asc' ? 'desc' : 'asc' }]
|
||||
if (previous == null)
|
||||
return [...sort, { key, direction: 'asc' }]
|
||||
return previous.direction === 'desc'
|
||||
? sort.filter (item => item.key !== key)
|
||||
: sort.map (item => item.key === key ? { key, direction: 'desc' } : item)
|
||||
}
|
||||
|
||||
|
||||
export const normaliseCountsParams = (
|
||||
@@ -40,25 +166,75 @@ export const normaliseCountsParams = (
|
||||
if (!['month', 'user'].includes (params.get ('axis') ?? ''))
|
||||
params.set ('axis', 'month')
|
||||
if (params.has ('columns'))
|
||||
{
|
||||
const available = columnsForCounts (resource)
|
||||
const columns = params.get ('columns')!.split (',')
|
||||
.filter (column => available.includes (column as CountColumn))
|
||||
if (columns.length > 0)
|
||||
params.set ('columns', [...new Set (columns)].join (','))
|
||||
else
|
||||
params.delete ('columns')
|
||||
}
|
||||
params.set ('columns', selectedCountColumns (resource, params).join (','))
|
||||
return params
|
||||
}
|
||||
|
||||
|
||||
export const countsRequestParams = (params: URLSearchParams): URLSearchParams => {
|
||||
const result = new URLSearchParams (params)
|
||||
for (const key of ['columns', 'children', 'child_sort'])
|
||||
result.delete (key)
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
export const fetchCounts = (
|
||||
resource: CountsResource,
|
||||
params: URLSearchParams,
|
||||
locale: LocaleCode,
|
||||
): Promise<CountsResult> => apiGet (`/counts/${resource}`, {
|
||||
locale, params: Object.fromEntries (params) })
|
||||
locale, params: Object.fromEntries (countsRequestParams (params)) })
|
||||
|
||||
|
||||
const stableValue = (value: unknown): unknown => {
|
||||
if (Array.isArray (value)) return value.map (stableValue)
|
||||
if (value != null && typeof value === 'object')
|
||||
{
|
||||
return Object.fromEntries (Object.entries (value)
|
||||
.sort (([a], [b]) => a < b ? -1 : (a > b ? 1 : 0))
|
||||
.map (([key, entry]) => [key, stableValue (entry)]))
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
|
||||
const compareValue = (first: unknown, second: unknown): number => {
|
||||
if (Array.isArray (first) && Array.isArray (second) && first.length !== second.length)
|
||||
return first.length - second.length
|
||||
if (typeof first === 'number' && typeof second === 'number') return first - second
|
||||
if (typeof first === 'boolean' && typeof second === 'boolean')
|
||||
return Number (first) - Number (second)
|
||||
const left = typeof first === 'object' ? JSON.stringify (stableValue (first)) : String (first)
|
||||
const right = typeof second === 'object'
|
||||
? JSON.stringify (stableValue (second)) : String (second)
|
||||
return left < right ? -1 : (left > right ? 1 : 0)
|
||||
}
|
||||
|
||||
|
||||
export const sortedCountChildren = (
|
||||
rows: CountRecord[],
|
||||
path: string,
|
||||
params: URLSearchParams,
|
||||
valueFor: (row: CountRecord, key: string) => unknown = (row, key) => row[key],
|
||||
): CountRecord[] => {
|
||||
const sort = readCountSort (params.get ('child_sort'))
|
||||
.filter (item => item.key.startsWith (`${path}/`))
|
||||
return rows.map ((row, index) => ({ row, index })).sort ((first, second) => {
|
||||
for (const item of sort)
|
||||
{
|
||||
const key = item.key.slice (path.length + 1)
|
||||
const left = valueFor (first.row, key)
|
||||
const right = valueFor (second.row, key)
|
||||
if (left == null && right != null) return 1
|
||||
if (left != null && right == null) return -1
|
||||
if (left == null && right == null) continue
|
||||
const compared = compareValue (left, right)
|
||||
if (compared !== 0) return item.direction === 'asc' ? compared : -compared
|
||||
}
|
||||
return first.index - second.index
|
||||
}).map (item => item.row)
|
||||
}
|
||||
|
||||
|
||||
export const countsCSV = (rows: string[][]): string =>
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
import { fieldsForCounts, selectedCountChildren, sortedCountChildren } from '@/lib/counts'
|
||||
import { translateUIMessage } from '@/lib/i18n'
|
||||
import { localisedDateString, msToTime, originalCreatedAtString } from '@/lib/utils'
|
||||
|
||||
import type { CountColumn, CountField, CountRecord,
|
||||
CountsResource, CountsVersion } from '@/lib/counts'
|
||||
import type { LocaleCode } from '@/lib/locales'
|
||||
|
||||
|
||||
export const countsCellText = (
|
||||
row: CountsVersion,
|
||||
column: CountColumn,
|
||||
locale: LocaleCode,
|
||||
): string => {
|
||||
switch (column)
|
||||
{
|
||||
case 'resource_id': return String (row.resourceId)
|
||||
case 'version_no': return String (row.versionNo)
|
||||
case 'is_latest':
|
||||
return translateUIMessage (`boolean.${row.isLatest}`, 'counts', locale).text
|
||||
case 'title': return row.title ?? ''
|
||||
case 'url': return row.url ?? ''
|
||||
case 'thumbnail':
|
||||
case 'thumbnail_base': return row.thumbnailBase ?? ''
|
||||
case 'name': return row.name ?? ''
|
||||
case 'category':
|
||||
return translateUIMessage (`common:categories.${row.category}`, 'counts', locale).text
|
||||
case 'video_ms': return row.videoMs == null ? '' : msToTime (row.videoMs)
|
||||
case 'original_created_at':
|
||||
return originalCreatedAtString (
|
||||
row.originalCreatedFrom ?? null, row.originalCreatedBefore ?? null, locale)
|
||||
case 'original_created_from':
|
||||
case 'original_created_before':
|
||||
{
|
||||
const value = column === 'original_created_from'
|
||||
? row.originalCreatedFrom : row.originalCreatedBefore
|
||||
return value == null ? '' : localisedDateString (value, locale)
|
||||
}
|
||||
case 'deprecated':
|
||||
return row.deprecated == null ? '' : localisedDateString (row.deprecated, locale)
|
||||
case 'user':
|
||||
if (row.createdByUser == null)
|
||||
return translateUIMessage ('history:bot', 'counts', locale).text
|
||||
return row.createdByUser.name
|
||||
? `${row.createdByUser.name} (#${row.createdByUser.id})`
|
||||
: translateUIMessage ('history:anonymous', 'counts', locale, {
|
||||
id: row.createdByUser.id }).text
|
||||
case 'created_at': return localisedDateString (row.createdAt, locale)
|
||||
default: return ''
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export const countsCSVCell = (
|
||||
row: CountsVersion,
|
||||
column: CountColumn,
|
||||
resource: CountsResource,
|
||||
params: URLSearchParams,
|
||||
locale: LocaleCode,
|
||||
): string => {
|
||||
const field = fieldsForCounts (resource).find (field => field.key === column)!
|
||||
if (field.children != null)
|
||||
{
|
||||
const rows = row[column as keyof CountsVersion] as CountRecord[] ?? []
|
||||
return JSON.stringify (countsNestedExport (rows, field.children, column, params, locale))
|
||||
}
|
||||
return countsCellText (row, column, locale)
|
||||
}
|
||||
|
||||
|
||||
const displayNames = new Map<string, Intl.DisplayNames> ()
|
||||
|
||||
|
||||
export const countsLogicalName = (
|
||||
row: CountRecord,
|
||||
field: string,
|
||||
locale: LocaleCode,
|
||||
): { text: string; language?: string } => {
|
||||
const value = row[field]
|
||||
if (value == null) return { text: '' }
|
||||
const code = String (value)
|
||||
if (field === 'languageCode' || field === 'scriptCode')
|
||||
{
|
||||
if (field === 'languageCode' && ['ja', 'ain'].includes (code))
|
||||
return translateUIMessage (`history:languageNames.${code}`, 'counts', locale)
|
||||
const master = row[field === 'languageCode' ? 'languageName' : 'scriptName']
|
||||
if (typeof master === 'string' && master.trim () !== ''
|
||||
&& master.toLowerCase () !== code.toLowerCase ())
|
||||
return { text: master }
|
||||
try
|
||||
{
|
||||
const type = field === 'languageCode' ? 'language' : 'script'
|
||||
if (!displayNames.has (type))
|
||||
displayNames.set (type, new Intl.DisplayNames (['ja'], { type, fallback: 'none' }))
|
||||
const text = displayNames.get (type)!.of (code)
|
||||
if (text != null && text !== code) return { text, language: 'ja' }
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Invalid or unregistered codes have no logical label; never expose the code as a name.
|
||||
}
|
||||
return translateUIMessage ('notRegistered', 'counts', locale)
|
||||
}
|
||||
if (field === 'kind' || field === 'category')
|
||||
return translateUIMessage (field === 'kind' ? `kinds.${code}`
|
||||
: `common:categories.${code}`, 'counts', locale)
|
||||
if (typeof value === 'boolean')
|
||||
return translateUIMessage (`boolean.${value}`, 'counts', locale)
|
||||
if (field === 'beginMs' || field === 'endMs') return { text: msToTime (Number (value)) }
|
||||
return { text: String (value) }
|
||||
}
|
||||
|
||||
|
||||
export const countsNestedOrder = (
|
||||
rows: CountRecord[],
|
||||
path: string,
|
||||
params: URLSearchParams,
|
||||
locale: LocaleCode,
|
||||
): CountRecord[] => sortedCountChildren (rows, path, params, (row, key) =>
|
||||
['languageCode', 'scriptCode'].includes (key)
|
||||
? countsLogicalName (row, key, locale).text : row[key])
|
||||
|
||||
|
||||
export const countsNestedExport = (
|
||||
rows: CountRecord[],
|
||||
fields: CountField[],
|
||||
path: string,
|
||||
params: URLSearchParams,
|
||||
locale: LocaleCode,
|
||||
): unknown => {
|
||||
const selected = selectedCountChildren (params)
|
||||
const columns = fields.filter (field => selected.has (`${path}.${field.key}`))
|
||||
if (columns.length === 0)
|
||||
return { [translateUIMessage ('common:countLabel', 'counts', locale).text]: rows.length }
|
||||
return countsNestedOrder (rows, path, params, locale).map (row =>
|
||||
Object.fromEntries (columns.map (field => [
|
||||
translateUIMessage (field.label, 'counts', locale).text,
|
||||
field.children
|
||||
? countsNestedExport (row[field.key] as CountRecord[] ?? [], field.children,
|
||||
`${path}.${field.key}`, params, locale)
|
||||
: countsLogicalName (row, field.key, locale).text])))
|
||||
}
|
||||
|
||||
|
||||
export const countsNestedText = (
|
||||
rows: CountRecord[],
|
||||
fields: CountField[],
|
||||
path: string,
|
||||
params: URLSearchParams,
|
||||
locale: LocaleCode,
|
||||
): string => {
|
||||
const selected = selectedCountChildren (params)
|
||||
const columns = fields.filter (field => selected.has (`${path}.${field.key}`))
|
||||
if (columns.length === 0)
|
||||
return `${translateUIMessage ('common:countLabel', 'counts', locale).text}: ${rows.length}`
|
||||
return countsNestedOrder (rows, path, params, locale).map (row => columns.map (field => {
|
||||
const value = field.children
|
||||
? countsNestedText (row[field.key] as CountRecord[] ?? [], field.children,
|
||||
`${path}.${field.key}`, params, locale)
|
||||
: countsLogicalName (row, field.key, locale).text
|
||||
return `${translateUIMessage (field.label, 'counts', locale).text}: ${value}`
|
||||
}).join (' | ')).join ('\n')
|
||||
}
|
||||
|
||||
|
||||
export const countsCellCopy = (
|
||||
row: CountsVersion,
|
||||
field: CountField,
|
||||
params: URLSearchParams,
|
||||
locale: LocaleCode,
|
||||
): string => field.children
|
||||
? countsNestedText (row[field.key as keyof CountsVersion] as CountRecord[] ?? [],
|
||||
field.children, field.key, params, locale)
|
||||
: countsCellText (row, field.key as CountColumn, locale)
|
||||
|
||||
|
||||
// Historical URLs are displayed without fetching current attachments or metadata.
|
||||
export const countsImageURL = (value: string | null | undefined): string | null => {
|
||||
if (!value) return null
|
||||
try
|
||||
{
|
||||
const url = new URL (value)
|
||||
return (['https:', 'http:'].includes (url.protocol) && !url.username && !url.password)
|
||||
? url.href : null
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { createContext, useContext } from 'react'
|
||||
|
||||
export const MIN_COUNT_WIDTH = 64
|
||||
export const MAX_COUNT_WIDTH = 2000
|
||||
|
||||
export const defaultCountWidths: Record<string, number> = {
|
||||
thumbnail: 88, resource_id: 88, version_no: 88, is_latest: 104,
|
||||
title: 300, url: 300, thumbnail_base: 300, tags: 560, parents: 180,
|
||||
video_ms: 120, original_created_at: 260, original_created_from: 240,
|
||||
original_created_before: 240, name: 240, names: 480, category: 180,
|
||||
aliases: 360, deprecated: 240, user: 200, created_at: 240, group: 240, count: 160,
|
||||
id: 88, versionNo: 88, kind: 140, sections: 240, beginMs: 120, endMs: 120,
|
||||
languageCode: 180, scriptCode: 200, autoGenerated: 120 }
|
||||
|
||||
type Layout = {
|
||||
width: (family: string, key: string) => number
|
||||
resize: (family: string, key: string, width: number) => void }
|
||||
export const CountsLayoutContext = createContext<Layout | null> (null)
|
||||
export const CountsTableScope = createContext ('')
|
||||
|
||||
|
||||
export const useCountsTableLayout = (): Layout => {
|
||||
const context = useContext (CountsLayoutContext)
|
||||
if (context == null) throw new Error ('Counts table layout provider is missing')
|
||||
return context
|
||||
}
|
||||
|
||||
|
||||
+26
-26
@@ -91,36 +91,36 @@ export const originalCreatedAtString = (
|
||||
const unknownLabel = (key: string) => i18n.t (`history:${key}`, { lng: locale })
|
||||
const space = locale === 'ja' ? ' ' : '\u3000'
|
||||
|
||||
if (from && before)
|
||||
const fromParts = from == null ? null : new Date (from.getTime () + 9 * 3_600_000)
|
||||
const beforeParts = before == null ? null : new Date (before.getTime () + 9 * 3_600_000)
|
||||
|
||||
if (from && before && fromParts && beforeParts)
|
||||
{
|
||||
const diff = before.getTime () - from.getTime ()
|
||||
if (diff <= 60_000 /* 1 分 */)
|
||||
return format (from, 'second')
|
||||
if (fromParts.getUTCMinutes () === 0 && beforeParts.getUTCMinutes () === 0)
|
||||
{
|
||||
const diff = before.getTime () - from.getTime ()
|
||||
|
||||
if (diff <= 60_000 /* 1 分 */)
|
||||
return format (from, 'second')
|
||||
|
||||
if (from.getMinutes () === 0 && before.getMinutes () === 0)
|
||||
if (Math.abs (diff - 3_600_000 /* 1 時間 */) < 60_000)
|
||||
return `${format (from, 'minute')}${space}(${unknownLabel ('unknownMinute')})`
|
||||
if (fromParts.getUTCHours () === 0 && beforeParts.getUTCHours () === 0)
|
||||
{
|
||||
if (Math.abs (diff - 86_400_000 /* 1 日 */) < 60_000)
|
||||
return `${format (from, 'hour')}${space}(${unknownLabel ('unknownTime')})`
|
||||
if (fromParts.getUTCDate () === 1 && beforeParts.getUTCDate () === 1)
|
||||
{
|
||||
if (Math.abs (diff - 3_600_000 /* 1 時間 */) < 60_000)
|
||||
return `${format (from, 'minute')}${space}(${unknownLabel ('unknownMinute')})`
|
||||
|
||||
if (from.getHours () === 0 && before.getHours () === 0)
|
||||
{
|
||||
if (Math.abs (diff - 86_400_000 /* 1 日 */) < 60_000)
|
||||
return `${format (from, 'hour')}${space}(${unknownLabel ('unknownTime')})`
|
||||
|
||||
if (from.getDate () === 1 && before.getDate () === 1)
|
||||
{
|
||||
if (2_332_800_000 /* 27 日 */ < diff && diff < 2_764_800_000 /* 32 日 */)
|
||||
return `${format (from, 'day')}${space}(${unknownLabel ('unknownDay')})`
|
||||
|
||||
if (from.getMonth () === 0 && before.getMonth () === 0
|
||||
&& (31_449_600_000 /* 364 日 */ <= diff
|
||||
&& diff < 31_708_800_000 /* 367 日 */))
|
||||
return `${format (from, 'month')}${space}(${unknownLabel ('unknownMonthDay')})`
|
||||
}
|
||||
}
|
||||
if (2_332_800_000 /* 27 日 */ < diff && diff < 2_764_800_000 /* 32 日 */)
|
||||
return `${format (from, 'day')}${space}(${unknownLabel ('unknownDay')})`
|
||||
if (fromParts.getUTCMonth () === 0 && beforeParts.getUTCMonth () === 0
|
||||
&& (31_449_600_000 /* 364 日 */ <= diff
|
||||
&& diff < 31_708_800_000 /* 367 日 */))
|
||||
{
|
||||
return `${format (from, 'month')}${space}(${unknownLabel ('unknownMonthDay')})`
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const rtn = ([from ? format (from, 'second') : '',
|
||||
'〜',
|
||||
|
||||
新しいイシューから参照
ユーザーをブロックする