@wtfalch/threads 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/bin/copy.d.ts +31 -0
- package/dist/bin/copy.js +58 -0
- package/dist/bin/migrations.d.ts +2 -0
- package/dist/bin/migrations.js +21 -0
- package/dist/gates.d.ts +48 -0
- package/dist/gates.js +38 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/migrations/0001_threads.sql +185 -0
- package/dist/react/CategoryList.d.ts +9 -0
- package/dist/react/CategoryList.js +7 -0
- package/dist/react/CommentForm.d.ts +19 -0
- package/dist/react/CommentForm.js +35 -0
- package/dist/react/CommentSection.d.ts +42 -0
- package/dist/react/CommentSection.js +26 -0
- package/dist/react/NewThreadForm.d.ts +18 -0
- package/dist/react/NewThreadForm.js +29 -0
- package/dist/react/Roadmap.d.ts +10 -0
- package/dist/react/Roadmap.js +12 -0
- package/dist/react/ThreadList.d.ts +21 -0
- package/dist/react/ThreadList.js +15 -0
- package/dist/react/ThreadView.d.ts +26 -0
- package/dist/react/ThreadView.js +20 -0
- package/dist/react/format.d.ts +16 -0
- package/dist/react/format.js +42 -0
- package/dist/react/index.d.ts +25 -0
- package/dist/react/index.js +17 -0
- package/dist/schema.d.ts +2085 -0
- package/dist/schema.js +129 -0
- package/dist/threads.css +182 -0
- package/dist/threads.d.ts +109 -0
- package/dist/threads.js +626 -0
- package/dist/tree.d.ts +47 -0
- package/dist/tree.js +75 -0
- package/package.json +72 -0
package/dist/schema.js
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { sql } from 'drizzle-orm';
|
|
2
|
+
import { bigint, boolean, customType, index, integer, pgTable, primaryKey, text, timestamp, uniqueIndex, } from 'drizzle-orm/pg-core';
|
|
3
|
+
/**
|
|
4
|
+
* The tables, mirrored from `migrations/0001_threads.sql`, which is the
|
|
5
|
+
* source of truth: the SQL carries the triggers, checks and partial indexes
|
|
6
|
+
* that drizzle-kit does not generate. An app spreads these into its own
|
|
7
|
+
* schema module so `db.query` and the types know them.
|
|
8
|
+
*
|
|
9
|
+
* Every user-owned row keys by the issuer's user id, a string.
|
|
10
|
+
*/
|
|
11
|
+
export const ltree = customType({
|
|
12
|
+
dataType() {
|
|
13
|
+
return 'ltree';
|
|
14
|
+
},
|
|
15
|
+
});
|
|
16
|
+
export const threadPeople = pgTable('thread_people', {
|
|
17
|
+
id: text('id').primaryKey(),
|
|
18
|
+
name: text('name'),
|
|
19
|
+
email: text('email'),
|
|
20
|
+
standing: text('standing', { enum: ['ok', 'muted', 'banned'] })
|
|
21
|
+
.notNull()
|
|
22
|
+
.default('ok'),
|
|
23
|
+
lastSeenAt: timestamp('last_seen_at', { withTimezone: true }).notNull().default(sql `now()`),
|
|
24
|
+
createdAt: timestamp('created_at', { withTimezone: true }).notNull().default(sql `now()`),
|
|
25
|
+
});
|
|
26
|
+
export const threadCategories = pgTable('thread_categories', {
|
|
27
|
+
id: bigint('id', { mode: 'number' }).primaryKey().generatedAlwaysAsIdentity(),
|
|
28
|
+
slug: text('slug').notNull().unique(),
|
|
29
|
+
name: text('name').notNull(),
|
|
30
|
+
description: text('description'),
|
|
31
|
+
kind: text('kind', { enum: ['discussion', 'feedback'] })
|
|
32
|
+
.notNull()
|
|
33
|
+
.default('discussion'),
|
|
34
|
+
position: integer('position').notNull().default(0),
|
|
35
|
+
createdAt: timestamp('created_at', { withTimezone: true }).notNull().default(sql `now()`),
|
|
36
|
+
});
|
|
37
|
+
export const threads = pgTable('threads', {
|
|
38
|
+
id: bigint('id', { mode: 'number' }).primaryKey().generatedAlwaysAsIdentity(),
|
|
39
|
+
subjectKind: text('subject_kind').notNull(),
|
|
40
|
+
subjectId: text('subject_id').notNull(),
|
|
41
|
+
authorId: text('author_id').references(() => threadPeople.id),
|
|
42
|
+
title: text('title'),
|
|
43
|
+
slug: text('slug'),
|
|
44
|
+
body: text('body'),
|
|
45
|
+
pinned: boolean('pinned').notNull().default(false),
|
|
46
|
+
locked: boolean('locked').notNull().default(false),
|
|
47
|
+
hidden: boolean('hidden').notNull().default(false),
|
|
48
|
+
status: text('status', { enum: ['open', 'planned', 'in_progress', 'done', 'declined'] }),
|
|
49
|
+
duplicateOf: bigint('duplicate_of', { mode: 'number' }),
|
|
50
|
+
commentCount: integer('comment_count').notNull().default(0),
|
|
51
|
+
voteCount: integer('vote_count').notNull().default(0),
|
|
52
|
+
lastActivityAt: timestamp('last_activity_at', { withTimezone: true })
|
|
53
|
+
.notNull()
|
|
54
|
+
.default(sql `now()`),
|
|
55
|
+
createdAt: timestamp('created_at', { withTimezone: true }).notNull().default(sql `now()`),
|
|
56
|
+
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().default(sql `now()`),
|
|
57
|
+
}, (t) => [
|
|
58
|
+
index('threads_subject_idx').on(t.subjectKind, t.subjectId, t.pinned, t.lastActivityAt),
|
|
59
|
+
uniqueIndex('threads_host_subject_idx')
|
|
60
|
+
.on(t.subjectKind, t.subjectId)
|
|
61
|
+
.where(sql `${t.subjectKind} <> 'category'`),
|
|
62
|
+
index('threads_author_idx').on(t.authorId, t.createdAt),
|
|
63
|
+
]);
|
|
64
|
+
export const threadComments = pgTable('thread_comments', {
|
|
65
|
+
id: bigint('id', { mode: 'number' }).primaryKey().generatedAlwaysAsIdentity(),
|
|
66
|
+
threadId: bigint('thread_id', { mode: 'number' })
|
|
67
|
+
.notNull()
|
|
68
|
+
.references(() => threads.id, { onDelete: 'cascade' }),
|
|
69
|
+
replyTo: bigint('reply_to', { mode: 'number' }),
|
|
70
|
+
/** Written by a trigger; never set it from here. */
|
|
71
|
+
path: ltree('path').notNull(),
|
|
72
|
+
authorId: text('author_id')
|
|
73
|
+
.notNull()
|
|
74
|
+
.references(() => threadPeople.id),
|
|
75
|
+
body: text('body').notNull(),
|
|
76
|
+
hidden: boolean('hidden').notNull().default(false),
|
|
77
|
+
createdAt: timestamp('created_at', { withTimezone: true }).notNull().default(sql `now()`),
|
|
78
|
+
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().default(sql `now()`),
|
|
79
|
+
}, (t) => [
|
|
80
|
+
index('thread_comments_thread_path_idx').on(t.threadId, t.path),
|
|
81
|
+
index('thread_comments_author_idx').on(t.authorId, t.createdAt),
|
|
82
|
+
]);
|
|
83
|
+
export const threadVotes = pgTable('thread_votes', {
|
|
84
|
+
threadId: bigint('thread_id', { mode: 'number' })
|
|
85
|
+
.notNull()
|
|
86
|
+
.references(() => threads.id, { onDelete: 'cascade' }),
|
|
87
|
+
personId: text('person_id')
|
|
88
|
+
.notNull()
|
|
89
|
+
.references(() => threadPeople.id),
|
|
90
|
+
createdAt: timestamp('created_at', { withTimezone: true }).notNull().default(sql `now()`),
|
|
91
|
+
}, (t) => [primaryKey({ columns: [t.threadId, t.personId] })]);
|
|
92
|
+
export const threadSubscriptions = pgTable('thread_subscriptions', {
|
|
93
|
+
threadId: bigint('thread_id', { mode: 'number' })
|
|
94
|
+
.notNull()
|
|
95
|
+
.references(() => threads.id, { onDelete: 'cascade' }),
|
|
96
|
+
personId: text('person_id')
|
|
97
|
+
.notNull()
|
|
98
|
+
.references(() => threadPeople.id),
|
|
99
|
+
reason: text('reason', { enum: ['author', 'commented', 'voted', 'manual'] }).notNull(),
|
|
100
|
+
muted: boolean('muted').notNull().default(false),
|
|
101
|
+
createdAt: timestamp('created_at', { withTimezone: true }).notNull().default(sql `now()`),
|
|
102
|
+
}, (t) => [primaryKey({ columns: [t.threadId, t.personId] })]);
|
|
103
|
+
export const threadOutbox = pgTable('thread_outbox', {
|
|
104
|
+
id: bigint('id', { mode: 'number' }).primaryKey().generatedAlwaysAsIdentity(),
|
|
105
|
+
personId: text('person_id')
|
|
106
|
+
.notNull()
|
|
107
|
+
.references(() => threadPeople.id),
|
|
108
|
+
threadId: bigint('thread_id', { mode: 'number' })
|
|
109
|
+
.notNull()
|
|
110
|
+
.references(() => threads.id, { onDelete: 'cascade' }),
|
|
111
|
+
commentId: bigint('comment_id', { mode: 'number' }).references(() => threadComments.id, {
|
|
112
|
+
onDelete: 'cascade',
|
|
113
|
+
}),
|
|
114
|
+
kind: text('kind', { enum: ['comment', 'status'] }).notNull(),
|
|
115
|
+
createdAt: timestamp('created_at', { withTimezone: true }).notNull().default(sql `now()`),
|
|
116
|
+
claimedAt: timestamp('claimed_at', { withTimezone: true }),
|
|
117
|
+
sentAt: timestamp('sent_at', { withTimezone: true }),
|
|
118
|
+
attempts: integer('attempts').notNull().default(0),
|
|
119
|
+
lastError: text('last_error'),
|
|
120
|
+
});
|
|
121
|
+
export const schema = {
|
|
122
|
+
threadPeople,
|
|
123
|
+
threadCategories,
|
|
124
|
+
threads,
|
|
125
|
+
threadComments,
|
|
126
|
+
threadVotes,
|
|
127
|
+
threadSubscriptions,
|
|
128
|
+
threadOutbox,
|
|
129
|
+
};
|
package/dist/threads.css
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* @wtfalch/threads — layout for the reference components. Says where things
|
|
3
|
+
* stand, never how they look: every value is a token from @wtfalch/design,
|
|
4
|
+
* so a forum in lokessmie draws lokessmie's without this file knowing.
|
|
5
|
+
* Classes carry the `threads-` prefix; the package's own are unprefixed.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
.threads-stack {
|
|
9
|
+
display: flex;
|
|
10
|
+
flex-direction: column;
|
|
11
|
+
gap: var(--space-4);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.threads-stack-tight {
|
|
15
|
+
display: flex;
|
|
16
|
+
flex-direction: column;
|
|
17
|
+
gap: var(--space-2);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.threads-meta {
|
|
21
|
+
color: var(--muted);
|
|
22
|
+
font-size: var(--text-sm);
|
|
23
|
+
display: flex;
|
|
24
|
+
flex-wrap: wrap;
|
|
25
|
+
gap: var(--space-2);
|
|
26
|
+
align-items: baseline;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.threads-meta a {
|
|
30
|
+
color: inherit;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.threads-title {
|
|
34
|
+
margin: 0;
|
|
35
|
+
font-size: var(--text-xl);
|
|
36
|
+
line-height: 1.25;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.threads-title a {
|
|
40
|
+
color: inherit;
|
|
41
|
+
text-decoration: none;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.threads-title a:hover {
|
|
45
|
+
text-decoration: underline;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.threads-head {
|
|
49
|
+
display: flex;
|
|
50
|
+
flex-direction: column;
|
|
51
|
+
gap: var(--space-2);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.threads-body {
|
|
55
|
+
padding-block: var(--space-2);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.threads-bar {
|
|
59
|
+
display: flex;
|
|
60
|
+
flex-wrap: wrap;
|
|
61
|
+
gap: var(--space-2);
|
|
62
|
+
align-items: center;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.threads-bar-grow {
|
|
66
|
+
flex: 1;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/* The tree. Each level is a rail down the left, drawn from the border token,
|
|
70
|
+
* so depth reads at a glance without a background per level. */
|
|
71
|
+
.threads-comments {
|
|
72
|
+
display: flex;
|
|
73
|
+
flex-direction: column;
|
|
74
|
+
gap: var(--space-3);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.threads-comment {
|
|
78
|
+
display: flex;
|
|
79
|
+
flex-direction: column;
|
|
80
|
+
gap: var(--space-2);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.threads-comment-head {
|
|
84
|
+
display: flex;
|
|
85
|
+
flex-wrap: wrap;
|
|
86
|
+
gap: var(--space-2);
|
|
87
|
+
align-items: baseline;
|
|
88
|
+
font-size: var(--text-sm);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.threads-comment-author {
|
|
92
|
+
font-weight: 600;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.threads-comment-body {
|
|
96
|
+
font-size: var(--text-md);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.threads-comment-hidden {
|
|
100
|
+
color: var(--muted);
|
|
101
|
+
font-style: italic;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.threads-comment-actions {
|
|
105
|
+
display: flex;
|
|
106
|
+
gap: var(--space-1);
|
|
107
|
+
align-items: center;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.threads-children {
|
|
111
|
+
margin-top: var(--space-2);
|
|
112
|
+
padding-left: var(--space-4);
|
|
113
|
+
border-left: 2px solid var(--border);
|
|
114
|
+
display: flex;
|
|
115
|
+
flex-direction: column;
|
|
116
|
+
gap: var(--space-3);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.threads-fold {
|
|
120
|
+
font-size: var(--text-sm);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.threads-form {
|
|
124
|
+
display: flex;
|
|
125
|
+
flex-direction: column;
|
|
126
|
+
gap: var(--space-3);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.threads-form-actions {
|
|
130
|
+
display: flex;
|
|
131
|
+
gap: var(--space-2);
|
|
132
|
+
justify-content: flex-end;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.threads-note {
|
|
136
|
+
color: var(--muted);
|
|
137
|
+
font-size: var(--text-sm);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.threads-section-title {
|
|
141
|
+
margin: 0;
|
|
142
|
+
font-size: var(--text-md);
|
|
143
|
+
color: var(--muted);
|
|
144
|
+
text-transform: uppercase;
|
|
145
|
+
letter-spacing: 0.04em;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.threads-vote {
|
|
149
|
+
display: inline-flex;
|
|
150
|
+
align-items: center;
|
|
151
|
+
gap: var(--space-1);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/* Links the package draws itself. A name is text that happens to be a link;
|
|
155
|
+
* everything smaller (a fold, a note, a time) takes the accent. */
|
|
156
|
+
.threads-link {
|
|
157
|
+
color: var(--text);
|
|
158
|
+
text-decoration: underline;
|
|
159
|
+
text-decoration-color: var(--border-strong, var(--border));
|
|
160
|
+
text-underline-offset: 0.15em;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.threads-link:hover {
|
|
164
|
+
text-decoration-color: currentColor;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.threads-fold a,
|
|
168
|
+
.threads-note a,
|
|
169
|
+
.threads-meta a {
|
|
170
|
+
color: var(--accent);
|
|
171
|
+
text-decoration: underline;
|
|
172
|
+
text-underline-offset: 0.15em;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
.threads-link:focus-visible,
|
|
176
|
+
.threads-fold a:focus-visible,
|
|
177
|
+
.threads-note a:focus-visible,
|
|
178
|
+
.threads-meta a:focus-visible {
|
|
179
|
+
outline: none;
|
|
180
|
+
border-radius: var(--radius-sm);
|
|
181
|
+
box-shadow: var(--focus-ring);
|
|
182
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import type { PgDatabase, PgQueryResultHKT } from 'drizzle-orm/pg-core';
|
|
2
|
+
import { type Gates, type Subject, type Who } from './gates.js';
|
|
3
|
+
import { type Category, type Comment, type Person, type Thread, type ThreadStatus, schema } from './schema.js';
|
|
4
|
+
/**
|
|
5
|
+
* The core: every query and every action, bound to one app's database and
|
|
6
|
+
* the gates the app passed. A page or a Server Action in the app calls these
|
|
7
|
+
* with the person from its session; nothing here reads a cookie or a request.
|
|
8
|
+
*
|
|
9
|
+
* The gate is applied here and nowhere else. A list, a count, a search or a
|
|
10
|
+
* thread page that forgets it leaks a private subject into a public result,
|
|
11
|
+
* so the only way to read is through a function in this file that asks first.
|
|
12
|
+
*/
|
|
13
|
+
export type Db = PgDatabase<PgQueryResultHKT, any>;
|
|
14
|
+
export interface Limits {
|
|
15
|
+
/** Threads one person may open per hour. */
|
|
16
|
+
threadsPerHour: number;
|
|
17
|
+
/** Comments one person may post per ten minutes. */
|
|
18
|
+
commentsPerTenMinutes: number;
|
|
19
|
+
}
|
|
20
|
+
export interface ThreadsOptions {
|
|
21
|
+
db: Db;
|
|
22
|
+
gates: Gates;
|
|
23
|
+
limits?: Partial<Limits>;
|
|
24
|
+
now?: () => Date;
|
|
25
|
+
}
|
|
26
|
+
export type Sort = 'activity' | 'newest' | 'votes';
|
|
27
|
+
export interface ListOptions {
|
|
28
|
+
page?: number;
|
|
29
|
+
perPage?: number;
|
|
30
|
+
sort?: Sort;
|
|
31
|
+
status?: ThreadStatus;
|
|
32
|
+
}
|
|
33
|
+
export interface ThreadRow extends Thread {
|
|
34
|
+
authorName: string | null;
|
|
35
|
+
}
|
|
36
|
+
export interface CommentRow extends Omit<Comment, 'body'> {
|
|
37
|
+
/** Null when hidden: the tree keeps its shape, the words are gone. */
|
|
38
|
+
body: string | null;
|
|
39
|
+
authorName: string | null;
|
|
40
|
+
}
|
|
41
|
+
export interface OutboxItem {
|
|
42
|
+
id: number;
|
|
43
|
+
kind: 'comment' | 'status';
|
|
44
|
+
to: {
|
|
45
|
+
id: string;
|
|
46
|
+
name: string | null;
|
|
47
|
+
email: string;
|
|
48
|
+
};
|
|
49
|
+
thread: Thread;
|
|
50
|
+
comment: Comment | null;
|
|
51
|
+
}
|
|
52
|
+
export declare const DEFAULT_LIMITS: Limits;
|
|
53
|
+
export declare function slugify(title: string): string;
|
|
54
|
+
export declare function createThreads(opts: ThreadsOptions): {
|
|
55
|
+
seen: (who: Who) => Promise<Person>;
|
|
56
|
+
categories: () => Promise<Category[]>;
|
|
57
|
+
category: (slug: string) => Promise<Category | null>;
|
|
58
|
+
categoryById: (id: number) => Promise<Category | null>;
|
|
59
|
+
createCategory: (who: Who | null, input: {
|
|
60
|
+
slug: string;
|
|
61
|
+
name: string;
|
|
62
|
+
description?: string;
|
|
63
|
+
kind?: Category["kind"];
|
|
64
|
+
position?: number;
|
|
65
|
+
}) => Promise<Category>;
|
|
66
|
+
listThreads: (subject: Subject, who: Who | null, options?: ListOptions) => Promise<{
|
|
67
|
+
items: ThreadRow[];
|
|
68
|
+
page: number;
|
|
69
|
+
perPage: number;
|
|
70
|
+
total: number;
|
|
71
|
+
}>;
|
|
72
|
+
thread: (id: number, who: Who | null) => Promise<ThreadRow>;
|
|
73
|
+
threadFor: (subject: Subject, who: Who | null) => Promise<ThreadRow | null>;
|
|
74
|
+
comments: (threadId: number, who: Who | null, options?: {
|
|
75
|
+
root?: number;
|
|
76
|
+
}) => Promise<CommentRow[]>;
|
|
77
|
+
search: (q: string, who: Who | null, options?: {
|
|
78
|
+
subjectKind?: string;
|
|
79
|
+
limit?: number;
|
|
80
|
+
}) => Promise<ThreadRow[]>;
|
|
81
|
+
roadmap: (who: Who | null) => Promise<Record<ThreadStatus, ThreadRow[]>>;
|
|
82
|
+
post: (who: Who | null, subject: Subject, input: {
|
|
83
|
+
title: string;
|
|
84
|
+
body: string;
|
|
85
|
+
}) => Promise<Thread>;
|
|
86
|
+
comment: (who: Who | null, threadId: number, input: {
|
|
87
|
+
body: string;
|
|
88
|
+
replyTo?: number | null;
|
|
89
|
+
}) => Promise<Comment>;
|
|
90
|
+
commentOn: (who: Who | null, subject: Subject, input: {
|
|
91
|
+
body: string;
|
|
92
|
+
replyTo?: number | null;
|
|
93
|
+
}) => Promise<Comment>;
|
|
94
|
+
vote: (who: Who | null, threadId: number) => Promise<void>;
|
|
95
|
+
unvote: (who: Who | null, threadId: number) => Promise<void>;
|
|
96
|
+
voted: (who: Who | null, threadIds: number[]) => Promise<Set<number>>;
|
|
97
|
+
subscribe: (who: Who | null, threadId: number, muted?: boolean) => Promise<void>;
|
|
98
|
+
pin: (who: Who | null, id: number, on?: boolean) => Promise<void>;
|
|
99
|
+
lock: (who: Who | null, id: number, on?: boolean) => Promise<void>;
|
|
100
|
+
hide: (who: Who | null, id: number, on?: boolean) => Promise<void>;
|
|
101
|
+
hideComment: (who: Who | null, commentId: number, hidden?: boolean) => Promise<void>;
|
|
102
|
+
move: (who: Who | null, threadId: number, categoryId: number) => Promise<void>;
|
|
103
|
+
setStatus: (who: Who | null, threadId: number, status: ThreadStatus) => Promise<void>;
|
|
104
|
+
markDuplicate: (who: Who | null, threadId: number, ofId: number) => Promise<void>;
|
|
105
|
+
setStanding: (who: Who | null, personId: string, standing: Person["standing"]) => Promise<void>;
|
|
106
|
+
drainOutbox: (send: (item: OutboxItem) => Promise<void>, batch?: number) => Promise<number>;
|
|
107
|
+
};
|
|
108
|
+
export type Threads = ReturnType<typeof createThreads>;
|
|
109
|
+
export { schema };
|