feat: a Discord source can remove the posts it took from people outside its poster list (#4486)
CI and images / extension-version (push) Successful in 3s
CI and images / lint (push) Successful in 3s
CI and images / extension-test (push) Successful in 20s
CI and images / frontend-build (push) Successful in 21s
CI and images / backend-lint-and-test (push) Successful in 33s
CI and images / integration (push) Successful in 2m39s
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 1m43s
CI and images / smoke-web (push) Successful in 54s
CI and images / promote (push) Successful in 2s
CI and images / extension-version (push) Successful in 3s
CI and images / lint (push) Successful in 3s
CI and images / extension-test (push) Successful in 20s
CI and images / frontend-build (push) Successful in 21s
CI and images / backend-lint-and-test (push) Successful in 33s
CI and images / integration (push) Successful in 2m39s
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 1m43s
CI and images / smoke-web (push) Successful in 54s
CI and images / promote (push) Successful in 2s
"Remove posts from other posters" on a Discord source with an "Only posts by" list previews what goes, per poster, then deletes it behind a typed token: - posts whose record names a poster not on the list (by id, username or display name); posts that record no poster are left alone and counted; - images found only on those posts; one also on a kept post stays, and a synthetic drop's link never keeps one; - their attachments, and every drop that absorbed one of them, so the grouping sweep regroups what remains. Preview and apply share one predicate, and a parity test holds them to it. The post record now also saves the poster's display name. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,135 @@
|
||||
<template>
|
||||
<!-- #4486. A Discord source's posts by people outside its "Only posts by"
|
||||
list, taken before the list existed. Preview first, always: nothing is
|
||||
deleted until the operator has seen the per-poster breakdown and typed
|
||||
the token that preview returned. -->
|
||||
<v-dialog :model-value="modelValue" max-width="560"
|
||||
@update:model-value="$emit('update:modelValue', $event)">
|
||||
<v-card>
|
||||
<v-card-title>Remove posts from other posters</v-card-title>
|
||||
<v-card-text>
|
||||
<div class="text-caption mb-2">{{ source.display_name || source.url }}</div>
|
||||
<v-progress-linear v-if="loading" indeterminate color="accent" class="mb-3" />
|
||||
<v-alert v-else-if="error" type="warning" variant="tonal" density="compact">
|
||||
{{ error }}
|
||||
</v-alert>
|
||||
<template v-else-if="result">
|
||||
<v-alert type="success" variant="tonal" density="compact">
|
||||
Removed {{ result.posts_deleted }} post(s), {{ result.images_deleted }} image(s),
|
||||
{{ result.attachments_deleted }} attachment(s) and {{ result.drops_deleted }}
|
||||
grouped post(s). Grouping rebuilds what remains on its next sweep.
|
||||
</v-alert>
|
||||
</template>
|
||||
<template v-else-if="preview">
|
||||
<p class="mb-2">
|
||||
Keeping posts by <strong>{{ preview.authors.join(', ') }}</strong>.
|
||||
</p>
|
||||
<p v-if="!preview.posts" class="fc-dim">
|
||||
Nothing to remove: every post with a known poster is by someone on the list.
|
||||
</p>
|
||||
<template v-else>
|
||||
<v-table density="compact" class="mb-3">
|
||||
<thead><tr><th>Poster</th><th class="text-right">Posts</th></tr></thead>
|
||||
<tbody>
|
||||
<tr v-for="p in preview.posters" :key="p.poster">
|
||||
<td>{{ p.poster }}</td>
|
||||
<td class="text-right">{{ p.posts }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</v-table>
|
||||
<p class="mb-1">
|
||||
{{ preview.posts }} post(s), {{ preview.images }} image(s) found only on them,
|
||||
{{ preview.attachments }} attachment(s).
|
||||
</p>
|
||||
<p v-if="preview.drops" class="mb-1">
|
||||
{{ preview.drops }} grouped post(s) that include them are ungrouped; the
|
||||
rest regroup on the next sweep.
|
||||
</p>
|
||||
<p class="fc-dim">
|
||||
Images that also belong to a kept post stay. If a name you want to keep
|
||||
appears above, add it to the source's "Only posts by" list first.
|
||||
</p>
|
||||
</template>
|
||||
<p v-if="preview.unknown_posts" class="fc-dim mt-2">
|
||||
{{ preview.unknown_posts }} older post(s) don't record who posted them and
|
||||
are left alone.
|
||||
</p>
|
||||
</template>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn variant="text" @click="$emit('update:modelValue', false)">
|
||||
{{ result ? 'Close' : 'Cancel' }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
v-if="preview && preview.posts && !result"
|
||||
color="error" variant="flat" :loading="busy"
|
||||
@click="confirmOpen = true"
|
||||
>Remove {{ preview.posts }} post(s)</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
<DestructiveConfirmModal
|
||||
v-if="preview"
|
||||
v-model="confirmOpen"
|
||||
action="delete" kind="other posters' posts" tier="C"
|
||||
:expected-token-override="preview.confirm_token"
|
||||
:projected-counts="{ posts: preview.posts, images: preview.images,
|
||||
attachments: preview.attachments, grouped: preview.drops }"
|
||||
@confirm="onConfirm"
|
||||
/>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue'
|
||||
import { useSourcesStore } from '../../stores/sources.js'
|
||||
import DestructiveConfirmModal from '../modal/DestructiveConfirmModal.vue'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: Boolean, default: false },
|
||||
source: { type: Object, required: true },
|
||||
})
|
||||
defineEmits(['update:modelValue'])
|
||||
const store = useSourcesStore()
|
||||
|
||||
const loading = ref(false)
|
||||
const busy = ref(false)
|
||||
const error = ref('')
|
||||
const preview = ref(null)
|
||||
const result = ref(null)
|
||||
const confirmOpen = ref(false)
|
||||
|
||||
function message(e) {
|
||||
return e?.body?.detail || e?.body?.error || e?.message || String(e)
|
||||
}
|
||||
|
||||
watch(() => props.modelValue, async (open) => {
|
||||
if (!open) return
|
||||
preview.value = null
|
||||
result.value = null
|
||||
error.value = ''
|
||||
loading.value = true
|
||||
try {
|
||||
preview.value = await store.previewOtherPosters(props.source.id)
|
||||
} catch (e) {
|
||||
error.value = message(e)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
})
|
||||
|
||||
async function onConfirm(token) {
|
||||
busy.value = true
|
||||
try {
|
||||
result.value = await store.removeOtherPosters(props.source.id, token)
|
||||
} catch (e) {
|
||||
error.value = message(e)
|
||||
} finally {
|
||||
busy.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.fc-dim { color: rgb(var(--v-theme-on-surface-variant)); }
|
||||
</style>
|
||||
Reference in New Issue
Block a user