Files
modrinth/apps/frontend/src/components/ui/thread/ThreadView.vue
T
693a371d61 feat: server management in app (#5628)
* start new server settings tabs

* update properties tab to match design

* better stying in general tab

* feat: add suffix input for hostname field

* implement tables for allocations and DNS records

* add tags for dns record type

* small gap adjustment

* polish advanced page

* adjust properties page hierarchy

* fix searching properties, empty state and projection radius appearing

* pnpm prepr

* update copy to match designs

* fix suffix input component

* style fixes and match heading size

* small fix

* fix search allocations placeholder

* adjust table styles

* move all installation settings helper text to below input

* update icon to use overflow menu buttons

* fix modal to be consistent

* open advanced properties when search

* remove other and custom properties, and update styles

* remove hide/show all java versions

* handle mc 26

* refactor: move server settings pages into /ui and add app ServerSettingsModal

* hook up server pages for app

* add server page header to app

* hook up server settings modal

* use large size

* fix card box shadow style

* fix hostname input for app

* fix app/website card containers

* implement external tabs for billing and admin billing

* fix save banner fixed to parent instead of page body

* remove unused prop to FriendsList causing warning in app

* fix client-only not available for app

* fix bottom cut off

* wire node auth

* implement full copy buttons

* dedup copy button tailwind styles

* fix hover class not working in @apply

* fix spacing

* fix error validation styles

* apply consistent styles and spacing

* feat: update hosting server card (#5609)

* fix type errors

* fix some stylesheets not imported for storybook

* add server listing stories

* add fix for frontend stylesheet imports

* remove props.

* convert copy code to use tailwind

* update server listing component styles

* update server info label styles

* start status/player count info label, more style updates and fixes

* add new server card buttons

* hook up server cards and implement updated styles

* hook up on download button

* fix tauri throwing error when api returns 204 No Content

* hook up purchase server modal in app

* fix upgrading state loading icon

* pnpm prepr

* filter out servers past 30 days after cancellation

* do not apply opacity on lock or spiner icons

* fix disabled server icon background

* update pending change stage

* handle known suspension states

* refactor: reduce code duplication for server listing

* update disabled state text color

* fix loading icon color

* clean up copy

* fix disabled opacity for server card

* update server listing files kept to be countdown

* implement resubscribe modal

* implement proper provisioning state for resubscribe

* fix duplicate attribute and pnpm prepr

* feat: add shared UI package auth DI

* feat: update purchase server flow (#5714)

* implement server list empty state component

* fix stories and adjust spacing

* implement select plan design refresh

* implement auth for empty server list

* use refs instead of reactive

* pnpm prepr

* fix auth usage for empty servers list

* move app auth provider setup to src/providers/setup

* pnpm prepr

* fix max height

* style fix

* fix getCreds no auth is blocking api client

* implement servers guest plan modal and signin which redirects back to modal's next step

* refactor guest plan select logic into provider

* implement sign in or create account popup

* remove force empty serverList

* add download button for suspended mod and generic

* add handling for when user logs out

* QA pass style fixes

* more consistent page styles

* fix duplicate export

* refactor: remove all fallback stuff from resubscribe modal

* implement shared download latest backup util

* i18n pass

* pnpm prepr

* fix region being selected if ping failed

* pnpm prepr

* feat: servers in app finalization (#5744)

* feat: start on shared console implementation into logs and overview pages

* fix: terminal gap issues

* feat: swap word wrap for full screen

* fix: stats cards alignment

* fix: stats

* feat: fix console clear + remove copy

* fix: lint

* fix: use reset not clear

* feat: shared server header & overview page for app and website (#5736)

* feat: implement shared server header for app and website

* feat: implement wrapped overview page with shared composable and hook it up

* pnpm prepr

* fix: bugs

* qa: cleanup

* feat: root.vue shared layout

* feat: delete old options pages + fix discovery frontend

* fix: discovery

* fix: misc style/layout issues

* fix page padding

* fix: modal height jankiness

* feat: implement server install content in app and server setup modal with DI

* fix: spacing

* remove servers in app feature flag

* Revert "remove servers in app feature flag"

This reverts commit 86e284c4bd.

* fix: qa

* feat: remove legacy components from apps/frontend/src/components/ui/servers

---------

Co-authored-by: Calum H. (IMB11) <contact@cal.engineer>

* qa pass (#5738)

* fix: qa

* feat: qa

* fix: server icon fetch fails due to global node auth race condition overriding each other

* fix: lint

* fix: server icon upload/sync and centralize logic

* fix: server settings modal not closing for server reset

* fix: better server sorting

* feat: copy address in server listing card

* fix: notification panel in modal and when overlapping with action bar

* fix: empty server list empty state flashing when refresh, fixed by adding isReady auth flag

* feat: use floating action bar for save banner

* fix: saving state in save bar

* fix: edit server icon styling

* fix: confirm modal to have consistent buttons

* feat: loading animation for server panel + caching improvements for app

* pnpm prepr

* feat: search page deduplication (#5754)

* fix: action bar behind modal

* fix: remove warning modal for stopping

* fix: server cards states

* we hate webkit we hate webkit

* fix: update allocation creation to not use modal

* fix: properties tab spacing and styles

* feat: add files tab copy

* fix: advanced properties icon

* fix: remove back to all servers link

* feat: add files tab link in copy

* fix: server header styles to be consistent with instance

* fix: add header icons back

* feat: update instance settings icon to be consistent

* fix: icon container

* feat: upload state persistence across tabs

* fix: server labels text wrapping

* fix: use surface-5 border

* fix: loading spinner showing with onboarding below

* feat: new server button shows purchase modal in website

* fix: billing page not showing quarterly interval

* fix: server downgrade not showing updated subscription notification

* fix: server settings invalidate saved state and remove server context provider since its already provided in the page

* pnpm prepr

* add stripe publishable key to app build

* feat: console highlighting

* fix: rename servers title to modrinth hosting

* feat: search fix

* fix: qa/styles

* fix: ip click active and remove power dont ask again

* fix: qa

* feat: highlighting fix console

* fix: disable conflicts action

* fix: error dismiss bug

* feat: modal clarification

* fix: files perms issue

* fix: lint

* feat: modal fix

* enable show uptime

* fix: add loading state to edit server icon

* fix: notification panel take in has sidebar from settings

* fix: consistency pass on app settings

* fix: consistency pass on instance settings

* pnpm prepr

* fix: nagivate to billing button in app to go to website

* fix: stripe return url in app causing app to open modrinth.com in tauri

* refactor: better show polling UI code

* fix: new server polling comparison to use server ids instead of length

* fix: buttonstyled story

* fix: button styling

* fix: content.vue regression

* feat: project url redirects

* fix: breadcrumbs

* fix: purchase with newly added card

* fix: console ordering problems

* fix: app-frontend missing env config and staging environment

* fix: log syncing for instances and server panel accidentally

* fix: QA issues

* fix: server page loading state

* fix: stats card logic

* fix: lint

* fix: qa

* fix: console height padding

* fix: terminal padding + loading indicator

* feat: update medal server listing styling

* fix: no upgrade button for medal server listing in app

* fix: go to overview instead of content tab after onboarding

* fix: qa

* fix: teleport modals to body

* fix: logs tab + qa

* fix: local storage for user preferences

* fix: qa loading indic

* feat: considitonal debug and trace

* fix: jump to top on install bug

* feat: swap out server hard drive icon to server stack icon

* feat: servers in app feature flag default true

* fix: highlight row ufll

* fix: webkit thing onto a tag

* fix: input field

* fix: clear fix

* fix: lint

* fix: fmt

* feat: improve share modal and bring it back for sharing log

* pnpm prepr

* fix: menu overflowing

* feat: remove servers in app feature flag

* fix: server stat charts no longer showing color

* fix: library nav no primary state

* fix: better modal height and width

* fix: highlighting bugs

* fix: empty states

* fix: delay import to fix overview page slow load on MacOS

* fix: medal server listing too bright on light mode

* fix: admon analysis + fix logs

* fix: bug

* fix: clear purchase intent from sign-in after closing modal

* performance: improve server manage stats loading by splitting reactivity

* fix: deploy + admon + disable highlighting

* fix: clippy

---------

Co-authored-by: tdgao <mr.trumgao@gmail.com>
Co-authored-by: Truman Gao <106889354+tdgao@users.noreply.github.com>

* feat: temp wrangler

* fix: lint

* fix: logs upload

* fix: console empty state and admon regressions

* fix: fields

* feat: log deleting + prefetch for Logs.vue

* feat: move delete before share

* feat: clear endpoint

* feat: we ball!

---------

Co-authored-by: Calum H. <calum@modrinth.com>
Co-authored-by: Calum H. (IMB11) <contact@cal.engineer>
2026-04-12 21:38:08 +00:00

227 lines
5.5 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 { ChevronDownIcon, 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 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>