Skip to content

Commit 9830655

Browse files
authored
feat(help): show open/closed status in thread title (#55)
* feat(help): show open/closed status in thread title Discord doesn't let bots control the display order of a forum post's applied tags, so the open/closed tag routinely collapses into "+N" on post cards. Mirror the status in the thread title with an emoji prefix (green open / red closed), which is always visible. The title is set before archiving/locking because renaming a thread unarchives it, which would otherwise reopen a just-closed post. Reverts the applied-tag reordering from #52 (and its src/lib/discord/ tags.ts helper, removed separately) since Discord ignores applied_tags order for card display, so that approach had no effect. * refactor(help): remove orderAppliedTags helper Unused after reverting the applied-tag reordering from #52. * feat(help): sync title status on manual tag changes Update the thread title emoji when an admin flips the open/closed tag via Edit Tags, handled in the existing ThreadUpdate reconciler. No-op when the title already matches, so it won't rename (and reopen) a thread closed via /close.
1 parent 2940c29 commit 9830655

5 files changed

Lines changed: 68 additions & 46 deletions

File tree

‎src/commands/util/close.ts‎

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
66
isHelpPost,
77
} from "@lib/discord/channels.js";
88
import { getCommandMention } from "@lib/discord/commands.js";
9-
import { orderAppliedTags } from "@lib/discord/tags.js";
9+
import { applyStatusToTitle } from "@lib/discord/status.js";
1010

1111
import {
1212
type ThreadChannel,
@@ -44,15 +44,23 @@ export async function handleIssueState(
4444

4545
const { tagToAdd, tagToRemove } = getTagsForCloseState(close);
4646

47+
const postTags = threadChannel.appliedTags;
48+
4749
try {
48-
// Add the target state tag, drop its opposite, and reorder so status
49-
// tags follow the channel's configured order (open/closed first).
50-
const nextTags = orderAppliedTags(threadChannel, [
51-
...threadChannel.appliedTags.filter((t) => t !== tagToRemove),
52-
tagToAdd,
53-
]);
54-
55-
await threadChannel.setAppliedTags(nextTags, "Thread lifecycle");
50+
// Update tags
51+
if (!postTags.includes(tagToAdd)) {
52+
postTags.push(tagToAdd);
53+
}
54+
55+
if (postTags.includes(tagToRemove)) {
56+
postTags.splice(postTags.indexOf(tagToRemove), 1);
57+
}
58+
59+
await threadChannel.setAppliedTags(postTags, "Thread lifecycle");
60+
61+
// Surface the status in the title so it is always visible regardless of
62+
// tag order. Renaming unarchives a thread, so do this before archiving.
63+
await applyStatusToTitle(threadChannel, close);
5664

5765
const reopenHint =
5866
close && !lock

‎src/events/channels.ts‎

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { config } from "../lib/config.js";
22
import { getTagsForCloseState } from "../commands/util/close.js";
33
import { isHelpPost } from "../lib/discord/channels.js";
44
import { applyWaitingTag } from "../lib/discord/help.js";
5-
import { orderAppliedTags } from "../lib/discord/tags.js";
5+
import { applyStatusToTitle } from "../lib/discord/status.js";
66

77
import { debounce } from "throttle-debounce";
88

@@ -36,12 +36,14 @@ const handleEvent = debounce(
3636
const { tagToRemove } = getTagsForCloseState(isClose);
3737
if (newThread.appliedTags.includes(tagToRemove)) {
3838
await newThread.setAppliedTags(
39-
orderAppliedTags(
40-
newThread,
41-
newThread.appliedTags.filter((t) => t !== tagToRemove),
42-
),
39+
newThread.appliedTags.filter((t) => t !== tagToRemove),
4340
);
4441
}
42+
43+
// Keep the title status in sync when an admin flips the status tag
44+
// manually. This is a no-op when the title already matches (e.g. the
45+
// change came from /close), so it won't rename an archived thread.
46+
await applyStatusToTitle(newThread, isClose);
4547
}
4648
}
4749
}
@@ -60,9 +62,7 @@ const handleEvent = debounce(
6062
const isClose = tag === config.helpChannel.closedTag;
6163
const { tagToRemove } = getTagsForCloseState(isClose);
6264
if (!newThread.appliedTags.includes(tagToRemove)) {
63-
await newThread.setAppliedTags(
64-
orderAppliedTags(newThread, [...newThread.appliedTags, tag]),
65-
);
65+
await newThread.setAppliedTags([...newThread.appliedTags, tag]);
6666
}
6767
}
6868
}
@@ -76,7 +76,9 @@ export default function registerEvents(client: Client) {
7676
return;
7777
}
7878

79-
// A new help post is waiting for the Coder team to respond.
79+
// A new help post is open and waiting for the Coder team to respond.
80+
// Set the title status before anything archives the thread.
81+
await applyStatusToTitle(thread, false);
8082
await applyWaitingTag(thread, false);
8183
});
8284

‎src/lib/discord/help.ts‎

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { config } from "@lib/config.js";
2-
import { orderAppliedTags } from "@lib/discord/tags.js";
32
import { isTeamMember } from "@lib/discord/users.js";
43

54
import {
@@ -42,12 +41,11 @@ export async function applyWaitingTag(
4241
if (alreadyCorrect) return;
4342

4443
// Forum posts allow at most 5 tags. Keep the desired tag and drop the
45-
// opposite one, ordering by the channel's tag configuration and trimming
46-
// any overflow from the lowest-priority tags.
47-
const nextTags = orderAppliedTags(thread, [
44+
// opposite one, trimming any overflow from the least recent tags.
45+
const nextTags = [
4846
desired,
4947
...thread.appliedTags.filter((t) => t !== desired && t !== opposite),
50-
]).slice(0, 5);
48+
].slice(0, 5);
5149

5250
await thread.setAppliedTags(nextTags, "Help post waiting state");
5351
}

‎src/lib/discord/status.ts‎

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { type ThreadChannel } from "discord.js";
2+
3+
// Discord does not let bots control the display order of a forum post's
4+
// applied tags, so the open/closed tag often collapses into "+N". A title
5+
// prefix is always visible, so we mirror the status there too.
6+
export const OPEN_PREFIX = "\uD83D\uDFE2";
7+
export const CLOSED_PREFIX = "\uD83D\uDD34";
8+
9+
const MAX_THREAD_NAME_LENGTH = 100;
10+
const STATUS_PREFIXES = [OPEN_PREFIX, CLOSED_PREFIX];
11+
12+
// Removes any leading status prefix (and following whitespace) from a name.
13+
function stripStatusPrefix(name: string): string {
14+
let base = name.trimStart();
15+
for (const prefix of STATUS_PREFIXES) {
16+
if (base.startsWith(prefix)) {
17+
base = base.slice(prefix.length).trimStart();
18+
}
19+
}
20+
return base;
21+
}
22+
23+
// Prefixes the thread title with the status emoji, replacing any existing one.
24+
// Discord unarchives a thread when it is renamed, so callers must apply the
25+
// status BEFORE archiving or locking, otherwise the post reopens.
26+
export async function applyStatusToTitle(
27+
thread: ThreadChannel,
28+
closed: boolean,
29+
): Promise<void> {
30+
const prefix = closed ? CLOSED_PREFIX : OPEN_PREFIX;
31+
const base = stripStatusPrefix(thread.name);
32+
const maxBaseLength = MAX_THREAD_NAME_LENGTH - prefix.length - 1;
33+
const nextName = `${prefix} ${base.slice(0, maxBaseLength)}`;
34+
35+
if (thread.name === nextName) return;
36+
await thread.setName(nextName);
37+
}

‎src/lib/discord/tags.ts‎

Lines changed: 0 additions & 23 deletions
This file was deleted.

0 commit comments

Comments
 (0)