mirror of
https://github.com/hcengineering/platform.git
synced 2024-11-29 09:09:15 +03:00
uberf-7389: instant transactions (#5941)
Signed-off-by: Alexey Zinoviev <alexey.zinoviev@xored.com>
This commit is contained in:
parent
51383caa92
commit
6af082e66f
@ -41,6 +41,7 @@
|
|||||||
"@hcengineering/model-notification": "^0.6.0",
|
"@hcengineering/model-notification": "^0.6.0",
|
||||||
"@hcengineering/model-view": "^0.6.0",
|
"@hcengineering/model-view": "^0.6.0",
|
||||||
"@hcengineering/model-workbench": "^0.6.1",
|
"@hcengineering/model-workbench": "^0.6.1",
|
||||||
|
"@hcengineering/model-presentation": "^0.6.0",
|
||||||
"@hcengineering/notification": "^0.6.23",
|
"@hcengineering/notification": "^0.6.23",
|
||||||
"@hcengineering/platform": "^0.6.11",
|
"@hcengineering/platform": "^0.6.11",
|
||||||
"@hcengineering/ui": "^0.6.15",
|
"@hcengineering/ui": "^0.6.15",
|
||||||
|
@ -24,6 +24,7 @@ import {
|
|||||||
type ObjectChatPanel,
|
type ObjectChatPanel,
|
||||||
type ThreadMessage
|
type ThreadMessage
|
||||||
} from '@hcengineering/chunter'
|
} from '@hcengineering/chunter'
|
||||||
|
import presentation from '@hcengineering/model-presentation'
|
||||||
import contact from '@hcengineering/contact'
|
import contact from '@hcengineering/contact'
|
||||||
import {
|
import {
|
||||||
type Class,
|
type Class,
|
||||||
@ -457,6 +458,10 @@ export function createModel (builder: Builder): void {
|
|||||||
presenter: chunter.component.ChatMessagePresenter
|
presenter: chunter.component.ChatMessagePresenter
|
||||||
})
|
})
|
||||||
|
|
||||||
|
builder.mixin(chunter.class.ChatMessage, core.class.Class, presentation.mixin.InstantTransactions, {
|
||||||
|
txClasses: [core.class.TxCreateDoc]
|
||||||
|
})
|
||||||
|
|
||||||
builder.mixin(chunter.class.ThreadMessage, core.class.Class, view.mixin.ObjectPresenter, {
|
builder.mixin(chunter.class.ThreadMessage, core.class.Class, view.mixin.ObjectPresenter, {
|
||||||
presenter: chunter.component.ThreadMessagePresenter
|
presenter: chunter.component.ThreadMessagePresenter
|
||||||
})
|
})
|
||||||
|
@ -13,9 +13,9 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
//
|
//
|
||||||
|
|
||||||
import { DOMAIN_MODEL, type Blob, type Class, type Doc, type Ref } from '@hcengineering/core'
|
import { DOMAIN_MODEL, type Tx, type Blob, type Class, type Doc, type Ref } from '@hcengineering/core'
|
||||||
import { Model, Prop, TypeRef, TypeString, type Builder } from '@hcengineering/model'
|
import { Mixin, Model, Prop, TypeRef, TypeString, type Builder } from '@hcengineering/model'
|
||||||
import core, { TDoc } from '@hcengineering/model-core'
|
import core, { TClass, TDoc } from '@hcengineering/model-core'
|
||||||
import { type Asset, type IntlString, type Resource } from '@hcengineering/platform'
|
import { type Asset, type IntlString, type Resource } from '@hcengineering/platform'
|
||||||
// Import types to prevent .svelte components to being exposed to type typescript.
|
// Import types to prevent .svelte components to being exposed to type typescript.
|
||||||
import {
|
import {
|
||||||
@ -33,7 +33,8 @@ import {
|
|||||||
type FilePreviewExtension,
|
type FilePreviewExtension,
|
||||||
type ObjectSearchCategory,
|
type ObjectSearchCategory,
|
||||||
type ObjectSearchContext,
|
type ObjectSearchContext,
|
||||||
type ObjectSearchFactory
|
type ObjectSearchFactory,
|
||||||
|
type InstantTransactions
|
||||||
} from '@hcengineering/presentation/src/types'
|
} from '@hcengineering/presentation/src/types'
|
||||||
import { type AnyComponent, type ComponentExtensionId } from '@hcengineering/ui/src/types'
|
import { type AnyComponent, type ComponentExtensionId } from '@hcengineering/ui/src/types'
|
||||||
import presentation from './plugin'
|
import presentation from './plugin'
|
||||||
@ -94,6 +95,11 @@ export class TFilePreviewExtension extends TComponentPointExtension implements F
|
|||||||
availabilityChecker?: Resource<() => Promise<boolean>>
|
availabilityChecker?: Resource<() => Promise<boolean>>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Mixin(presentation.mixin.InstantTransactions, core.class.Class)
|
||||||
|
export class TInstantTransactions extends TClass implements InstantTransactions {
|
||||||
|
txClasses!: Array<Ref<Class<Tx>>>
|
||||||
|
}
|
||||||
|
|
||||||
export function createModel (builder: Builder): void {
|
export function createModel (builder: Builder): void {
|
||||||
builder.createModel(
|
builder.createModel(
|
||||||
TObjectSearchCategory,
|
TObjectSearchCategory,
|
||||||
@ -101,6 +107,7 @@ export function createModel (builder: Builder): void {
|
|||||||
TComponentPointExtension,
|
TComponentPointExtension,
|
||||||
TDocCreateExtension,
|
TDocCreateExtension,
|
||||||
TDocRules,
|
TDocRules,
|
||||||
TFilePreviewExtension
|
TFilePreviewExtension,
|
||||||
|
TInstantTransactions
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
//
|
//
|
||||||
|
|
||||||
import { type Class, type Ref } from '@hcengineering/core'
|
import { type Mixin, type Class, type Ref } from '@hcengineering/core'
|
||||||
import type { Asset, IntlString, Metadata, Plugin, StatusCode } from '@hcengineering/platform'
|
import type { Asset, IntlString, Metadata, Plugin, StatusCode } from '@hcengineering/platform'
|
||||||
import { plugin } from '@hcengineering/platform'
|
import { plugin } from '@hcengineering/platform'
|
||||||
import { type ComponentExtensionId } from '@hcengineering/ui'
|
import { type ComponentExtensionId } from '@hcengineering/ui'
|
||||||
@ -24,7 +24,8 @@ import {
|
|||||||
type DocRules,
|
type DocRules,
|
||||||
type DocCreateExtension,
|
type DocCreateExtension,
|
||||||
type FilePreviewExtension,
|
type FilePreviewExtension,
|
||||||
type ObjectSearchCategory
|
type ObjectSearchCategory,
|
||||||
|
type InstantTransactions
|
||||||
} from './types'
|
} from './types'
|
||||||
import type { PreviewConfig } from './preview'
|
import type { PreviewConfig } from './preview'
|
||||||
|
|
||||||
@ -42,6 +43,9 @@ export default plugin(presentationId, {
|
|||||||
DocRules: '' as Ref<Class<DocRules>>,
|
DocRules: '' as Ref<Class<DocRules>>,
|
||||||
FilePreviewExtension: '' as Ref<Class<FilePreviewExtension>>
|
FilePreviewExtension: '' as Ref<Class<FilePreviewExtension>>
|
||||||
},
|
},
|
||||||
|
mixin: {
|
||||||
|
InstantTransactions: '' as Ref<Mixin<InstantTransactions>>
|
||||||
|
},
|
||||||
string: {
|
string: {
|
||||||
Create: '' as IntlString,
|
Create: '' as IntlString,
|
||||||
Cancel: '' as IntlString,
|
Cancel: '' as IntlString,
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
|
type Tx,
|
||||||
type Blob,
|
type Blob,
|
||||||
type Class,
|
type Class,
|
||||||
type Client,
|
type Client,
|
||||||
@ -183,3 +184,10 @@ export interface FilePreviewExtension extends ComponentPointExtension {
|
|||||||
// Extension is only available if this checker returns true
|
// Extension is only available if this checker returns true
|
||||||
availabilityChecker?: Resource<() => Promise<boolean>>
|
availabilityChecker?: Resource<() => Promise<boolean>>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export interface InstantTransactions extends Class<Doc> {
|
||||||
|
txClasses: Array<Ref<Class<Tx>>>
|
||||||
|
}
|
||||||
|
@ -17,9 +17,11 @@
|
|||||||
import { Analytics } from '@hcengineering/analytics'
|
import { Analytics } from '@hcengineering/analytics'
|
||||||
import core, {
|
import core, {
|
||||||
TxOperations,
|
TxOperations,
|
||||||
|
TxProcessor,
|
||||||
concatLink,
|
concatLink,
|
||||||
getCurrentAccount,
|
getCurrentAccount,
|
||||||
reduceCalls,
|
reduceCalls,
|
||||||
|
type TxApplyIf,
|
||||||
type AnyAttribute,
|
type AnyAttribute,
|
||||||
type ArrOf,
|
type ArrOf,
|
||||||
type AttachedDoc,
|
type AttachedDoc,
|
||||||
@ -46,7 +48,8 @@ import core, {
|
|||||||
type Tx,
|
type Tx,
|
||||||
type TxResult,
|
type TxResult,
|
||||||
type TypeAny,
|
type TypeAny,
|
||||||
type WithLookup
|
type WithLookup,
|
||||||
|
type TxCUD
|
||||||
} from '@hcengineering/core'
|
} from '@hcengineering/core'
|
||||||
import { getMetadata, getResource } from '@hcengineering/platform'
|
import { getMetadata, getResource } from '@hcengineering/platform'
|
||||||
import { LiveQuery as LQ } from '@hcengineering/query'
|
import { LiveQuery as LQ } from '@hcengineering/query'
|
||||||
@ -54,14 +57,14 @@ import { getRawCurrentLocation, workspaceId, type AnyComponent, type AnySvelteCo
|
|||||||
import view, { type AttributeCategory, type AttributeEditor } from '@hcengineering/view'
|
import view, { type AttributeCategory, type AttributeEditor } from '@hcengineering/view'
|
||||||
import { deepEqual } from 'fast-equals'
|
import { deepEqual } from 'fast-equals'
|
||||||
import { onDestroy } from 'svelte'
|
import { onDestroy } from 'svelte'
|
||||||
import { get } from 'svelte/store'
|
import { type Writable, get, writable } from 'svelte/store'
|
||||||
import { type KeyedAttribute } from '..'
|
import { type KeyedAttribute } from '..'
|
||||||
import { OptimizeQueryMiddleware, PresentationPipelineImpl, type PresentationPipeline } from './pipeline'
|
import { OptimizeQueryMiddleware, PresentationPipelineImpl, type PresentationPipeline } from './pipeline'
|
||||||
import plugin from './plugin'
|
import plugin from './plugin'
|
||||||
export { reduceCalls } from '@hcengineering/core'
|
export { reduceCalls } from '@hcengineering/core'
|
||||||
|
|
||||||
let liveQuery: LQ
|
let liveQuery: LQ
|
||||||
let client: TxOperations & MeasureClient
|
let client: TxOperations & MeasureClient & OptimisticTxes
|
||||||
let pipeline: PresentationPipeline
|
let pipeline: PresentationPipeline
|
||||||
|
|
||||||
const txListeners: Array<(...tx: Tx[]) => void> = []
|
const txListeners: Array<(...tx: Tx[]) => void> = []
|
||||||
@ -83,7 +86,11 @@ export function removeTxListener (l: (tx: Tx) => void): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class UIClient extends TxOperations implements Client, MeasureClient {
|
export interface OptimisticTxes {
|
||||||
|
pendingCreatedDocs: Writable<Record<Ref<Doc>, boolean>>
|
||||||
|
}
|
||||||
|
|
||||||
|
class UIClient extends TxOperations implements Client, MeasureClient, OptimisticTxes {
|
||||||
constructor (
|
constructor (
|
||||||
client: MeasureClient,
|
client: MeasureClient,
|
||||||
private readonly liveQuery: Client
|
private readonly liveQuery: Client
|
||||||
@ -93,23 +100,56 @@ class UIClient extends TxOperations implements Client, MeasureClient {
|
|||||||
|
|
||||||
afterMeasure: Tx[] = []
|
afterMeasure: Tx[] = []
|
||||||
measureOp?: MeasureDoneOperation
|
measureOp?: MeasureDoneOperation
|
||||||
|
protected pendingTxes = new Set<Ref<Tx>>()
|
||||||
|
protected _pendingCreatedDocs = writable<Record<Ref<Doc>, boolean>>({})
|
||||||
|
|
||||||
|
get pendingCreatedDocs (): typeof this._pendingCreatedDocs {
|
||||||
|
return this._pendingCreatedDocs
|
||||||
|
}
|
||||||
|
|
||||||
async doNotify (...tx: Tx[]): Promise<void> {
|
async doNotify (...tx: Tx[]): Promise<void> {
|
||||||
if (this.measureOp !== undefined) {
|
if (this.measureOp !== undefined) {
|
||||||
this.afterMeasure.push(...tx)
|
this.afterMeasure.push(...tx)
|
||||||
} else {
|
} else {
|
||||||
try {
|
const pending = get(this._pendingCreatedDocs)
|
||||||
await pipeline.notifyTx(...tx)
|
let pendingUpdated = false
|
||||||
|
tx.forEach((t) => {
|
||||||
|
if (this.pendingTxes.has(t._id)) {
|
||||||
|
this.pendingTxes.delete(t._id)
|
||||||
|
|
||||||
await liveQuery.tx(...tx)
|
// Only CUD tx can be pending now
|
||||||
|
const innerTx = TxProcessor.extractTx(t) as TxCUD<Doc>
|
||||||
|
|
||||||
txListeners.forEach((it) => {
|
if (innerTx._class === core.class.TxCreateDoc) {
|
||||||
it(...tx)
|
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
||||||
})
|
delete pending[innerTx.objectId]
|
||||||
} catch (err: any) {
|
pendingUpdated = true
|
||||||
Analytics.handleError(err)
|
}
|
||||||
console.log(err)
|
}
|
||||||
|
})
|
||||||
|
if (pendingUpdated) {
|
||||||
|
this._pendingCreatedDocs.set(pending)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We still want to notify about all transactions because there might be queries created after
|
||||||
|
// the early applied transaction
|
||||||
|
// For old queries there's a check anyway that prevents the same document from being added twice
|
||||||
|
await this.provideNotify(...tx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async provideNotify (...tx: Tx[]): Promise<void> {
|
||||||
|
try {
|
||||||
|
await pipeline.notifyTx(...tx)
|
||||||
|
|
||||||
|
await liveQuery.tx(...tx)
|
||||||
|
|
||||||
|
txListeners.forEach((it) => {
|
||||||
|
it(...tx)
|
||||||
|
})
|
||||||
|
} catch (err: any) {
|
||||||
|
Analytics.handleError(err)
|
||||||
|
console.log(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -130,9 +170,49 @@ class UIClient extends TxOperations implements Client, MeasureClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override async tx (tx: Tx): Promise<TxResult> {
|
override async tx (tx: Tx): Promise<TxResult> {
|
||||||
|
void this.notifyEarly(tx)
|
||||||
|
|
||||||
return await this.client.tx(tx)
|
return await this.client.tx(tx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async notifyEarly (tx: Tx): Promise<void> {
|
||||||
|
if (tx._class === core.class.TxApplyIf) {
|
||||||
|
const applyTx = tx as TxApplyIf
|
||||||
|
|
||||||
|
if (applyTx.match.length !== 0 || applyTx.notMatch.length !== 0) {
|
||||||
|
// Cannot early apply conditional transactions
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
applyTx.txes.map(async (atx) => {
|
||||||
|
await this.notifyEarly(atx)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.getHierarchy().isDerived(tx._class, core.class.TxCUD)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const innerTx = TxProcessor.extractTx(tx) as TxCUD<Doc>
|
||||||
|
// Can pre-build some configuration later from the model if this will be too slow.
|
||||||
|
const instantTxes = this.getHierarchy().classHierarchyMixin(innerTx.objectClass, plugin.mixin.InstantTransactions)
|
||||||
|
if (instantTxes?.txClasses.includes(innerTx._class) !== true) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (innerTx._class === core.class.TxCreateDoc) {
|
||||||
|
const pending = get(this._pendingCreatedDocs)
|
||||||
|
pending[innerTx.objectId] = true
|
||||||
|
this._pendingCreatedDocs.set(pending)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.pendingTxes.add(tx._id)
|
||||||
|
await this.provideNotify(tx)
|
||||||
|
}
|
||||||
|
|
||||||
async searchFulltext (query: SearchQuery, options: SearchOptions): Promise<SearchResult> {
|
async searchFulltext (query: SearchQuery, options: SearchOptions): Promise<SearchResult> {
|
||||||
return await this.client.searchFulltext(query, options)
|
return await this.client.searchFulltext(query, options)
|
||||||
}
|
}
|
||||||
@ -159,7 +239,7 @@ class UIClient extends TxOperations implements Client, MeasureClient {
|
|||||||
/**
|
/**
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
export function getClient (): TxOperations & MeasureClient {
|
export function getClient (): TxOperations & MeasureClient & OptimisticTxes {
|
||||||
return client
|
return client
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -49,6 +49,8 @@
|
|||||||
export let hideFooter = false
|
export let hideFooter = false
|
||||||
export let skipLabel = false
|
export let skipLabel = false
|
||||||
export let hoverable = true
|
export let hoverable = true
|
||||||
|
export let pending = false
|
||||||
|
export let stale = false
|
||||||
export let hoverStyles: 'borderedHover' | 'filledHover' = 'borderedHover'
|
export let hoverStyles: 'borderedHover' | 'filledHover' = 'borderedHover'
|
||||||
export let showDatePreposition = false
|
export let showDatePreposition = false
|
||||||
export let type: ActivityMessageViewType = 'default'
|
export let type: ActivityMessageViewType = 'default'
|
||||||
@ -158,6 +160,7 @@
|
|||||||
class:actionsOpened={isActionsOpened}
|
class:actionsOpened={isActionsOpened}
|
||||||
class:borderedHover={hoverStyles === 'borderedHover'}
|
class:borderedHover={hoverStyles === 'borderedHover'}
|
||||||
class:filledHover={hoverStyles === 'filledHover'}
|
class:filledHover={hoverStyles === 'filledHover'}
|
||||||
|
class:stale
|
||||||
on:click={onClick}
|
on:click={onClick}
|
||||||
on:contextmenu={handleContextMenu}
|
on:contextmenu={handleContextMenu}
|
||||||
>
|
>
|
||||||
@ -226,7 +229,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if withActions && !readonly}
|
{#if withActions && !readonly}
|
||||||
<div class="actions" class:opened={isActionsOpened}>
|
<div class="actions" class:pending class:opened={isActionsOpened}>
|
||||||
<ActivityMessageActions
|
<ActivityMessageActions
|
||||||
message={isReactionMessage(message) ? parentMessage : message}
|
message={isReactionMessage(message) ? parentMessage : message}
|
||||||
{actions}
|
{actions}
|
||||||
@ -282,13 +285,15 @@
|
|||||||
top: -0.75rem;
|
top: -0.75rem;
|
||||||
right: 0.75rem;
|
right: 0.75rem;
|
||||||
|
|
||||||
&.opened {
|
&.opened:not(.pending) {
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover > .actions {
|
&:hover > .actions {
|
||||||
visibility: visible;
|
&:not(.pending) {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover > .time {
|
&:hover > .time {
|
||||||
@ -324,6 +329,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.stale {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
|
@ -100,8 +100,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function onMessage (event: CustomEvent) {
|
async function onMessage (event: CustomEvent) {
|
||||||
loading = true
|
if (chatMessage) {
|
||||||
const doneOp = await getClient().measure(`chunter.create.${_class} ${object._class}`)
|
loading = true
|
||||||
|
} // for new messages we use instant txes
|
||||||
|
|
||||||
|
const doneOp = getClient().measure(`chunter.create.${_class} ${object._class}`)
|
||||||
try {
|
try {
|
||||||
draftController.remove()
|
draftController.remove()
|
||||||
inputRef.removeDraft(false)
|
inputRef.removeDraft(false)
|
||||||
@ -116,11 +119,11 @@
|
|||||||
currentMessage = getDefault()
|
currentMessage = getDefault()
|
||||||
_id = currentMessage._id
|
_id = currentMessage._id
|
||||||
const d1 = Date.now()
|
const d1 = Date.now()
|
||||||
void doneOp().then((res) => {
|
void (await doneOp)().then((res) => {
|
||||||
console.log(`create.${_class} measure`, res, Date.now() - d1)
|
console.log(`create.${_class} measure`, res, Date.now() - d1)
|
||||||
})
|
})
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
void doneOp()
|
void (await doneOp)()
|
||||||
Analytics.handleError(err)
|
Analytics.handleError(err)
|
||||||
console.error(err)
|
console.error(err)
|
||||||
}
|
}
|
||||||
|
@ -54,8 +54,9 @@
|
|||||||
export let onClick: (() => void) | undefined = undefined
|
export let onClick: (() => void) | undefined = undefined
|
||||||
|
|
||||||
const client = getClient()
|
const client = getClient()
|
||||||
|
const { pendingCreatedDocs } = client
|
||||||
const hierarchy = client.getHierarchy()
|
const hierarchy = client.getHierarchy()
|
||||||
|
const STALE_TIMEOUT_MS = 5000
|
||||||
const userQuery = createQuery()
|
const userQuery = createQuery()
|
||||||
const currentAccount = getCurrentAccount()
|
const currentAccount = getCurrentAccount()
|
||||||
|
|
||||||
@ -103,6 +104,21 @@
|
|||||||
|
|
||||||
$: links = showLinksPreview ? getLinks(value?.message) : []
|
$: links = showLinksPreview ? getLinks(value?.message) : []
|
||||||
|
|
||||||
|
let stale = false
|
||||||
|
let markStaleId: NodeJS.Timeout | undefined
|
||||||
|
$: pending = value?._id !== undefined && $pendingCreatedDocs[value._id]
|
||||||
|
$: if (pending) {
|
||||||
|
markStaleId = setTimeout(() => {
|
||||||
|
stale = true
|
||||||
|
}, STALE_TIMEOUT_MS)
|
||||||
|
} else {
|
||||||
|
if (markStaleId !== undefined) {
|
||||||
|
clearTimeout(markStaleId)
|
||||||
|
markStaleId = undefined
|
||||||
|
}
|
||||||
|
stale = false
|
||||||
|
}
|
||||||
|
|
||||||
function getLinks (content?: string): HTMLLinkElement[] {
|
function getLinks (content?: string): HTMLLinkElement[] {
|
||||||
if (!content) {
|
if (!content) {
|
||||||
return []
|
return []
|
||||||
@ -185,6 +201,8 @@
|
|||||||
{hoverable}
|
{hoverable}
|
||||||
{hoverStyles}
|
{hoverStyles}
|
||||||
{skipLabel}
|
{skipLabel}
|
||||||
|
{pending}
|
||||||
|
{stale}
|
||||||
showDatePreposition={hideLink}
|
showDatePreposition={hideLink}
|
||||||
{type}
|
{type}
|
||||||
{onClick}
|
{onClick}
|
||||||
|
Loading…
Reference in New Issue
Block a user