You've already forked AstralRinth
forked from didirus/AstralRinth
* 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>
228 lines
5.6 KiB
Vue
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>
|