Files
AstralRinth/apps/frontend/src/components/ui/thread/ThreadView.vue
aecsocket 39f2b0ecb6 Technical review queue (#4775)
* chore: fix typo in status message

* feat(labrinth): overhaul malware scanner report storage and routes

* chore: address some review comments

* feat: add Delphi to Docker Compose `with-delphi` profile

* chore: fix unused import Clippy lint

* feat(labrinth/delphi): use PAT token authorization with project read scopes

* chore: expose file IDs in version queries

* fix: accept null decompiled source payloads from Delphi

* tweak(labrinth): expose base62 file IDs more consistently for Delphi

* feat(labrinth/delphi): support new Delphi report severity field

* chore(labrinth): run `cargo sqlx prepare` to fix Docker build errors

* tweak: add route for fetching Delphi issue type schema, abstract Labrinth away from issue types

* chore: run `cargo sqlx prepare`

* chore: fix typo on frontend generated state file message

* feat: update to use new Delphi issue schema

* wip: tech review endpoints

* wip: add ToSchema for dependent types

* wip: report issues return

* wip

* wip: returning more data

* wip

* Fix up db query

* Delphi configuration to talk to Labrinth

* Get Delphi working with Labrinth

* Add Delphi dummy fixture

* Better Delphi logging

* Improve utoipa for tech review routes

* Add more sorting options for tech review queue

* Oops join

* New routes for fetching issues and reports

* Fix which kind of ID is returned in tech review endpoints

* Deduplicate tech review report rows

* Reduce info sent for projects

* Fetch more thread info

* Address PR comments

* fix ci

* fix postgres version mismatch

* fix version creation

* Implement routes

* fix up tech review

* Allow adding a moderation comment to Delphi rejections

* fix up rebase

* exclude rejected projects from tech review

* add status change msg to tech review thread

* cargo sqlx prepare

* also ignore withheld projects

* More filtering on issue search

* wip: report routes

* Fix up for build

* cargo sqlx prepare

* fix thread message privacy

* New tech review search route

* submit route

* details have statuses now

* add default to drid status

* dedup issue details

* fix sqlx query on empty files

* fixes

* Dedupe issue detail statuses and message on entering tech rev

* Fix qa issues

* Fix qa issues

* fix review comments

* typos

* fix ci

* feat: tech review frontend (#4781)

* chore: fix typo in status message

* feat(labrinth): overhaul malware scanner report storage and routes

* chore: address some review comments

* feat: add Delphi to Docker Compose `with-delphi` profile

* chore: fix unused import Clippy lint

* feat(labrinth/delphi): use PAT token authorization with project read scopes

* chore: expose file IDs in version queries

* fix: accept null decompiled source payloads from Delphi

* tweak(labrinth): expose base62 file IDs more consistently for Delphi

* feat(labrinth/delphi): support new Delphi report severity field

* chore(labrinth): run `cargo sqlx prepare` to fix Docker build errors

* tweak: add route for fetching Delphi issue type schema, abstract Labrinth away from issue types

* chore: run `cargo sqlx prepare`

* chore: fix typo on frontend generated state file message

* feat: update to use new Delphi issue schema

* wip: tech review endpoints

* wip: add ToSchema for dependent types

* wip: report issues return

* wip

* wip: returning more data

* wip

* Fix up db query

* Delphi configuration to talk to Labrinth

* Get Delphi working with Labrinth

* Add Delphi dummy fixture

* Better Delphi logging

* Improve utoipa for tech review routes

* Add more sorting options for tech review queue

* Oops join

* New routes for fetching issues and reports

* Fix which kind of ID is returned in tech review endpoints

* Deduplicate tech review report rows

* Reduce info sent for projects

* Fetch more thread info

* Address PR comments

* fix ci

* fix ci

* fix postgres version mismatch

* fix version creation

* Implement routes

* feat: batch scan alert

* feat: layout

* feat: introduce surface variables

* fix: theme selector

* feat: rough draft of tech review card

* feat: tab switcher

* feat: batch scan btn

* feat: api-client module for tech review

* draft: impl

* feat: auto icons

* fix: layout issues

* feat: fixes to code blocks + flag labels

* feat: temp remove mock data

* fix: search sort types

* fix: intl & lint

* chore: re-enable mock data

* fix: flag badges + auto open first issue in file tab

* feat: update for new routes

* fix: more qa issues

* feat: lazy load sources

* fix: re-enable auth middleware

* feat: impl threads

* fix: lint & severity

* feat: download btn + switch to using NavTabs with new local mode option

* feat: re-add toplevel btns

* feat: reports page consistency

* fix: consistency on project queue

* fix: icons + sizing

* fix: colors and gaps

* fix: impl endpoints

* feat: load all flags on file tab

* feat: thread generics changes

* feat: more qa

* feat: fix collapse

* fix: qa

* feat: msg modal

* fix: ISO import

* feat: qa fixes

* fix: empty state basic

* fix: collapsible region

* fix: collapse thread by default

* feat: rough draft of new process/flow

* fix labrinth build

* fix thread message privacy

* New tech review search route

* feat: qa fixes

* feat: QA changes

* fix: verdict on detail not whole issue

* fix: lint + intl

* fix: lint

* fix: thread message for tech rev verdict

* feat: use anim frames

* fix: exports + typecheck

* polish: qa changes

* feat: qa

* feat: qa polish

* feat: fix malic modal

* fix: lint

* fix: qa + lint

* fix: pagination

* fix: lint

* fix: qa

* intl extract

* fix ci

---------

Signed-off-by: Calum H. <contact@cal.engineer>
Co-authored-by: Alejandro González <me@alegon.dev>
Co-authored-by: aecsocket <aecsocket@tutanota.com>

---------

Signed-off-by: Calum H. <contact@cal.engineer>
Co-authored-by: Alejandro González <me@alegon.dev>
Co-authored-by: Calum H. <contact@cal.engineer>
2025-12-20 11:43:04 +00:00

228 lines
5.6 KiB
Vue

<template>
<div>
<div v-if="flags.developerMode" class="mt-4 font-bold text-heading">
Thread ID:
<CopyCode :text="thread.id" />
</div>
<div v-if="sortedMessages.length > 0" class="flex flex-col space-y-4 rounded-xl p-3 sm:p-4">
<ThreadMessage
v-for="message in sortedMessages"
:key="'message-' + message.id"
:thread="thread"
:message="message"
:members="members"
:auth="auth"
raised
@update-thread="() => updateThreadLocal()"
/>
</div>
<div v-else class="flex flex-col items-center justify-center space-y-3 py-12">
<MessageIcon class="size-12 text-secondary" />
<p class="text-lg text-secondary">No messages yet</p>
</div>
<template v-if="closed">
<p class="text-secondary">This thread is closed and new messages cannot be sent to it.</p>
<slot name="closedActions" />
</template>
<template v-else>
<div>
<MarkdownEditor
v-model="replyBody"
:placeholder="sortedMessages.length > 0 ? 'Reply to thread...' : 'Send a message...'"
:on-image-upload="onUploadImage"
/>
</div>
<div
class="mt-4 flex flex-col items-stretch justify-between gap-3 sm:flex-row sm:items-center sm:gap-2"
>
<div class="flex flex-col items-stretch gap-2 sm:flex-row sm:items-center">
<ButtonStyled v-if="sortedMessages.length > 0" color="brand">
<button :disabled="!replyBody" class="w-full gap-2 sm:w-auto" @click="sendReply()">
<ReplyIcon class="size-4" />
Reply
</button>
</ButtonStyled>
<ButtonStyled v-else color="brand">
<button :disabled="!replyBody" class="w-full gap-2 sm:w-auto" @click="sendReply()">
<SendIcon class="size-4" />
Send
</button>
</ButtonStyled>
<ButtonStyled v-if="isStaff(auth.user)">
<button :disabled="!replyBody" class="w-full sm:w-auto" @click="sendReply(true)">
Add note
</button>
</ButtonStyled>
<ButtonStyled v-if="visibleQuickReplies.length > 0">
<OverflowMenu :options="visibleQuickReplies">
Quick Reply
<ChevronDownIcon />
</OverflowMenu>
</ButtonStyled>
</div>
<div class="flex flex-col items-stretch gap-2 sm:flex-row sm:items-center">
<slot name="additionalActions" :has-reply="!!replyBody" />
</div>
</div>
</template>
</div>
</template>
<script setup lang="ts" generic="T">
import { MessageIcon, ReplyIcon, SendIcon } from '@modrinth/assets'
import type { QuickReply } from '@modrinth/moderation'
import {
ButtonStyled,
CopyCode,
injectNotificationManager,
MarkdownEditor,
OverflowMenu,
type OverflowMenuOption,
} from '@modrinth/ui'
import type { Thread, User } from '@modrinth/utils'
import dayjs from 'dayjs'
import { useImageUpload } from '~/composables/image-upload.ts'
import { isStaff } from '~/helpers/users.js'
import ChevronDownIcon from '../servers/icons/ChevronDownIcon.vue'
import ThreadMessage from './ThreadMessage.vue'
const { addNotification } = injectNotificationManager()
const visibleQuickReplies = computed<OverflowMenuOption[]>(() => {
const replies = props.quickReplies
const context = props.quickReplyContext
if (!replies || !context) return []
return replies
.filter((reply) => {
if (reply.shouldShow === undefined) return true
return reply.shouldShow(context)
})
.map(
(reply) =>
({
id: reply.label,
action: () => handleQuickReply(reply, context),
}) as OverflowMenuOption,
)
})
const props = defineProps<{
thread: Thread
quickReplies?: ReadonlyArray<QuickReply<T>>
quickReplyContext?: T
closed?: boolean
}>()
async function handleQuickReply(reply: QuickReply<T>, context: T) {
const message = typeof reply.message === 'function' ? await reply.message(context) : reply.message
await nextTick()
setReplyContent(message)
}
defineExpose({
setReplyContent,
getReplyContent,
sendReply,
})
const auth = await useAuth()
const emit = defineEmits<{
updateThread: [thread: Thread]
}>()
const flags = useFeatureFlags()
const members = computed(() => {
const membersMap: Record<string, User> = {}
for (const member of props.thread.members) {
membersMap[member.id] = member
}
return membersMap
})
const replyBody = ref('')
function setReplyContent(content: string) {
replyBody.value = content
}
function getReplyContent(): string {
return replyBody.value
}
const sortedMessages = computed(() => {
if (!props.thread) return []
return [...props.thread.messages].sort(
(a, b) => dayjs(a.created).toDate().getTime() - dayjs(b.created).toDate().getTime(),
)
})
async function updateThreadLocal() {
const threadId = props.thread.id
if (threadId) {
try {
const thread = (await useBaseFetch(`thread/${threadId}`)) as Thread
emit('updateThread', thread)
} catch (error) {
console.error('Failed to update thread:', error)
}
}
}
const imageIDs = ref<string[]>([])
async function onUploadImage(file: File) {
const response = await useImageUpload(file, { context: 'thread_message' })
imageIDs.value.push(response.id)
imageIDs.value = imageIDs.value.slice(-10)
return response.url
}
async function sendReply(privateMessage = false) {
try {
const body: any = {
body: {
type: 'text',
body: replyBody.value,
private: privateMessage,
},
}
if (imageIDs.value.length > 0) {
body.body = {
...body.body,
uploaded_images: imageIDs.value,
}
}
await useBaseFetch(`thread/${props.thread.id}`, {
method: 'POST',
body,
})
replyBody.value = ''
await updateThreadLocal()
} catch (err: any) {
addNotification({
title: 'Error sending message',
text: err.data ? err.data.description : err,
type: 'error',
})
}
}
</script>