feat: the unified card shows referenced images, their text, and an undo (4402)
CI and images / lint (push) Successful in 3s
CI and images / extension-version (push) Successful in 3s
CI and images / frontend-build (push) Successful in 18s
CI and images / backend-lint-and-test (push) Successful in 32s
CI and images / integration (push) Successful in 2m22s
CI and images / sign-extension (push) Successful in 3s
CI and images / build-agent (push) Successful in 6s
CI and images / build-web (push) Successful in 1m38s
CI and images / smoke-web (push) Successful in 59s
CI and images / promote (push) Skipped

The linked drop's images and the piece's variants join the teaser's rail,
after its own images, each with a corner badge. The meta line counts them
apart ("+N from Discord"), so a Patreon card never reads as though the creator
posted Discord's files there. The text of each referenced post sits under
the teaser's body.

If FC made the link, the card says so, names the matching working name and
offers Undo. If a person accepted it, it offers Unlink and makes no claim
about FC. Both dismiss the pair. The kept row then stops the next sweep from
linking it straight back.

The announcement settings card gets the two windows: how close a drop must be
before its own card is left out of the feed, and how far to reach for
variants.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LVjrnpQjRgHdvq95rASoiR
This commit is contained in:
2026-09-24 11:25:52 -04:00
co-authored by Claude Opus 5.5
parent 30a263a47a
commit 7aa074d1a9
5 changed files with 328 additions and 9 deletions
+18 -2
View File
@@ -20,6 +20,9 @@ export const usePostAssociationsStore = defineStore('postAssociations', () => {
const auto = ref(true)
const threshold = ref(0.6)
const windowHours = ref(24)
// #4402 — what a link shows once made. See PostAssociationsCard.
const foldHours = ref(24)
const familyDays = ref(60)
const { loading, error, run } = useAsyncAction({ errorAs: 'message' })
async function load () {
@@ -35,6 +38,8 @@ export const usePostAssociationsStore = defineStore('postAssociations', () => {
auto.value = s.discord_link_auto
threshold.value = s.discord_link_threshold
windowHours.value = s.discord_link_window_hours
foldHours.value = s.discord_link_fold_hours
familyDays.value = s.discord_family_window_days
}
async function saveSettings (patch) {
@@ -61,6 +66,16 @@ export const usePostAssociationsStore = defineStore('postAssociations', () => {
await saveSettings({ discord_link_window_hours: v })
}
async function setFoldHours (v) {
foldHours.value = v
await saveSettings({ discord_link_fold_hours: v })
}
async function setFamilyDays (v) {
familyDays.value = v
await saveSettings({ discord_family_window_days: v })
}
async function accept (id) {
try {
await api.post(`/api/posts/associations/${id}/accept`, {})
@@ -86,8 +101,9 @@ export const usePostAssociationsStore = defineStore('postAssociations', () => {
}
return {
proposals, enabled, auto, threshold, windowHours, loading, error,
proposals, enabled, auto, threshold, windowHours, foldHours, familyDays,
loading, error,
load, loadSettings, setEnabled, setAuto, setThreshold, setWindowHours,
accept, dismiss, rescan
setFoldHours, setFamilyDays, accept, dismiss, rescan
}
})
+30 -1
View File
@@ -89,6 +89,35 @@ export const usePostsStore = defineStore('posts', () => {
return res
}
// Undo a link on a unified card (#4402). The operator chose "nest
// automatically, with visible undo" — so the undo is the review queue's own
// dismiss, whose kept row is what stops the next sweep linking the pair
// straight back. Patches the loaded teaser in place so the card drops its
// references at once; the drop's own card is not in the loaded page (it was
// folded out), so it returns on the next load rather than being guessed into
// position here.
async function unlink(postId, associationId) {
await api.post(`/api/posts/associations/${associationId}/dismiss`, {})
const item = items.value.find((p) => p.id === postId)
if (!item) return
item.associations = (item.associations || []).filter((a) => a.id !== associationId)
const unified = item.unified
if (!unified) return
const gone = unified.links.find((l) => l.association_id === associationId)
const links = unified.links.filter((l) => l.association_id !== associationId)
// Variants are keyed on the whole seed, so with one link left they may no
// longer all belong; with none left there is nothing to show at all. Drop
// what is certainly gone and let the next load recompute the rest.
item.unified = links.length
? {
...unified,
links,
thumbnails: unified.thumbnails.filter((t) => t.post_id !== gone?.post_id),
texts: unified.texts.filter((t) => t.post_id !== gone?.post_id),
}
: null
}
// Filter overlay for the around/older/newer (in-context anchored)
// path. Keep this distinct from `filters.value` (the down-only feed)
// so a normal-feed filter change doesn't leak into an active anchored
@@ -186,7 +215,7 @@ export const usePostsStore = defineStore('posts', () => {
return {
items, cursor, loading, done, error, filters,
cursorOlder, cursorNewer, doneOlder, doneNewer, anchorId,
loadInitial, loadMore, getPostFull, applyTranslationOverride,
loadInitial, loadMore, getPostFull, applyTranslationOverride, unlink,
loadAround, loadOlder, loadNewer,
}
})