Merge commit '175b90be5a42e5bfd3289ffdfbf7b201404f82a8' into beta

This commit is contained in:
2025-07-28 22:49:16 +03:00
47 changed files with 1186 additions and 297 deletions

View File

@@ -1,29 +1,28 @@
<script setup lang="ts">
import { ButtonStyled } from "@modrinth/ui";
import { MailIcon, CheckIcon } from "@modrinth/assets";
import { ref, watchEffect } from "vue";
import { ref } from "vue";
import { useBaseFetch } from "~/composables/fetch.js";
const auth = await useAuth();
const showSubscriptionConfirmation = ref(false);
const subscribed = ref(false);
async function checkSubscribed() {
if (auth.value?.user) {
try {
const { data } = await useBaseFetch("auth/email/subscribe", {
method: "GET",
});
subscribed.value = data?.subscribed || false;
} catch {
subscribed.value = false;
const showSubscribeButton = useAsyncData(
async () => {
if (auth.value?.user) {
try {
const { subscribed } = await useBaseFetch("auth/email/subscribe", {
method: "GET",
});
return !subscribed;
} catch {
return true;
}
} else {
return false;
}
}
}
watchEffect(() => {
checkSubscribed();
});
},
{ watch: [auth], server: false },
);
async function subscribe() {
try {
@@ -35,14 +34,19 @@ async function subscribe() {
} finally {
setTimeout(() => {
showSubscriptionConfirmation.value = false;
subscribed.value = true;
showSubscribeButton.status.value = "success";
showSubscribeButton.data.value = false;
}, 2500);
}
}
</script>
<template>
<ButtonStyled v-if="auth?.user && !subscribed" color="brand" type="outlined">
<ButtonStyled
v-if="showSubscribeButton.status.value === 'success' && showSubscribeButton.data.value"
color="brand"
type="outlined"
>
<button v-tooltip="`Subscribe to the Modrinth newsletter`" @click="subscribe">
<template v-if="!showSubscriptionConfirmation"> <MailIcon /> Subscribe </template>
<template v-else> <CheckIcon /> Subscribed! </template>

View File

@@ -1,13 +1,21 @@
<template>
<template v-if="moderation">
<Chips v-model="reasonFilter" :items="reasons" />
<p v-if="reports.length === MAX_REPORTS" class="text-red">
There are at least {{ MAX_REPORTS }} open reports. This page is at its max reports and will
not show any more recent ones.
</p>
<p v-else-if="reasonFilter === 'All'">There are {{ filteredReports.length }} open reports.</p>
<p v-else>
There are {{ filteredReports.length }}/{{ reports.length }} open '{{ reasonFilter }}' reports.
</p>
</template>
<ReportInfo
v-for="report in reports.filter(
(x) =>
(moderation || x.reporterUser.id === auth.user.id) &&
(viewMode === 'open' ? x.open : !x.open),
)"
v-for="report in filteredReports"
:key="report.id"
:report="report"
:thread="report.thread"
:show-message="false"
:moderation="moderation"
raised
:auth="auth"
@@ -16,11 +24,12 @@
<p v-if="reports.length === 0">You don't have any active reports.</p>
</template>
<script setup>
import { Chips } from "@modrinth/ui";
import ReportInfo from "~/components/ui/report/ReportInfo.vue";
import { addReportMessage } from "~/helpers/threads.js";
import { asEncodedJsonArray, fetchSegmented } from "~/utils/fetch-helpers.ts";
defineProps({
const props = defineProps({
moderation: {
type: Boolean,
default: false,
@@ -32,9 +41,14 @@ defineProps({
});
const viewMode = ref("open");
const reasonFilter = ref("All");
const reports = ref([]);
let { data: rawReports } = await useAsyncData("report", () => useBaseFetch("report?count=1000"));
const MAX_REPORTS = 1500;
let { data: rawReports } = await useAsyncData("report", () =>
useBaseFetch(`report?count=${MAX_REPORTS}`),
);
rawReports = rawReports.value.map((report) => {
report.item_id = report.item_id.replace(/"/g, "");
@@ -51,6 +65,7 @@ const userIds = [...new Set(reporterUsers.concat(reportedUsers))];
const threadIds = [
...new Set(rawReports.filter((report) => report.thread_id).map((report) => report.thread_id)),
];
const reasons = ["All", ...new Set(rawReports.map((report) => report.report_type))];
const [{ data: users }, { data: versions }, { data: threads }] = await Promise.all([
await useAsyncData(`users?ids=${JSON.stringify(userIds)}`, () =>
@@ -93,4 +108,13 @@ reports.value = rawReports.map((report) => {
report.open = true;
return report;
});
const filteredReports = computed(() =>
reports.value?.filter(
(x) =>
(props.moderation || x.reporterUser.id === props.auth.user.id) &&
(viewMode.value === "open" ? x.open : !x.open) &&
(reasonFilter.value === "All" || reasonFilter.value === x.report_type),
),
);
</script>

View File

@@ -66,6 +66,27 @@
<UiServersPanelSpinner />
Your server's hardware is currently being upgraded and will be back online shortly.
</div>
<div
v-else-if="status === 'suspended' && suspension_reason === 'cancelled'"
class="relative -mt-4 flex w-full flex-col gap-2 rounded-b-3xl bg-bg-red p-4 text-sm font-bold text-contrast"
>
<div class="flex flex-row gap-2">
<UiServersIconsPanelErrorIcon class="!size-5" /> Your server has been cancelled. Please
update your billing information or contact Modrinth Support for more information.
</div>
<CopyCode :text="`${props.server_id}`" class="ml-auto" />
</div>
<div
v-else-if="status === 'suspended' && suspension_reason"
class="relative -mt-4 flex w-full flex-col gap-2 rounded-b-3xl bg-bg-red p-4 text-sm font-bold text-contrast"
>
<div class="flex flex-row gap-2">
<UiServersIconsPanelErrorIcon class="!size-5" /> Your server has been suspended:
{{ suspension_reason }}. Please update your billing information or contact Modrinth Support
for more information.
</div>
<CopyCode :text="`${props.server_id}`" class="ml-auto" />
</div>
<div
v-else-if="status === 'suspended'"
class="relative -mt-4 flex w-full flex-col gap-2 rounded-b-3xl bg-bg-red p-4 text-sm font-bold text-contrast"
@@ -87,7 +108,8 @@ import { Avatar, CopyCode } from "@modrinth/ui";
const props = defineProps<Partial<Server>>();
if (props.server_id) {
if (props.server_id && props.status === "available") {
// Necessary only to get server icon
await useModrinthServers(props.server_id, ["general"]);
}
@@ -109,11 +131,6 @@ if (props.upstream) {
}
const image = useState<string | undefined>(`server-icon-${props.server_id}`, () => undefined);
if (import.meta.server && projectData.value?.icon_url) {
await useModrinthServers(props.server_id!, ["general"]);
}
const iconUrl = computed(() => projectData.value?.icon_url || undefined);
const isConfiguring = computed(() => props.flows?.intro);
</script>

View File

@@ -34,6 +34,38 @@
</div>
</div>
</Modal>
<Modal ref="modalReply" header="Reply to thread">
<div class="modal-submit universal-body">
<span>
Your project is already approved. As such, the moderation team does not actively monitor
this thread. However, they may still see your message if there is a problem with your
project.
</span>
<span>
If you need to get in contact with the moderation team, please use the
<a class="text-link" href="https://support.modrinth.com" target="_blank">
Modrinth Help Center
</a>
and click the green bubble to contact support.
</span>
<Checkbox
v-model="replyConfirmation"
description="Confirm moderators do not actively monitor this"
>
I acknowledge that the moderators do not actively monitor the thread.
</Checkbox>
<div class="input-group push-right">
<button
class="btn btn-primary"
:disabled="!replyConfirmation"
@click="sendReplyFromModal()"
>
<ReplyIcon aria-hidden="true" />
Reply to thread
</button>
</div>
</div>
</Modal>
<div v-if="flags.developerMode" class="thread-id">
Thread ID:
<CopyCode :text="thread.id" />
@@ -71,12 +103,17 @@
v-if="sortedMessages.length > 0"
class="btn btn-primary"
:disabled="!replyBody"
@click="sendReply()"
@click="isApproved(project) && !isStaff(auth.user) ? openReplyModal() : sendReply()"
>
<ReplyIcon aria-hidden="true" />
Reply
</button>
<button v-else class="btn btn-primary" :disabled="!replyBody" @click="sendReply()">
<button
v-else
class="btn btn-primary"
:disabled="!replyBody"
@click="isApproved(project) && !isStaff(auth.user) ? openReplyModal() : sendReply()"
>
<SendIcon aria-hidden="true" />
Send
</button>
@@ -289,6 +326,7 @@ const sortedMessages = computed(() => {
});
const modalSubmit = ref(null);
const modalReply = ref(null);
async function updateThreadLocal() {
let threadId = null;
@@ -316,6 +354,11 @@ async function onUploadImage(file) {
return response.url;
}
async function sendReplyFromModal(status = null, privateMessage = false) {
modalReply.value.hide();
await sendReply(status, privateMessage);
}
async function sendReply(status = null, privateMessage = false) {
try {
const body = {
@@ -398,6 +441,7 @@ async function reopenReport() {
const replyWithSubmission = ref(false);
const submissionConfirmation = ref(false);
const replyConfirmation = ref(false);
function openResubmitModal(reply) {
submissionConfirmation.value = false;
@@ -405,6 +449,11 @@ function openResubmitModal(reply) {
modalSubmit.value.show();
}
function openReplyModal(reply) {
replyConfirmation.value = false;
modalReply.value.show();
}
async function resubmit() {
if (replyWithSubmission.value) {
await sendReply("processing");

View File

@@ -182,9 +182,6 @@
"collection.button.unfollow-project": {
"message": "Unfollow project"
},
"collection.button.upload-icon": {
"message": "Upload icon"
},
"collection.delete-modal.description": {
"message": "This will remove this collection forever. This action cannot be undone."
},

View File

@@ -951,14 +951,7 @@ import {
useRelativeTime,
} from "@modrinth/ui";
import VersionSummary from "@modrinth/ui/src/components/version/VersionSummary.vue";
import {
formatCategory,
formatProjectType,
isRejected,
isStaff,
isUnderReview,
renderString,
} from "@modrinth/utils";
import { formatCategory, formatProjectType, renderString } from "@modrinth/utils";
import dayjs from "dayjs";
import { Tooltip } from "floating-vue";
import { useLocalStorage } from "@vueuse/core";
@@ -1646,9 +1639,7 @@ const navLinks = computed(() => {
{
label: formatMessage(messages.moderationTab),
href: `${projectUrl}/moderation`,
shown:
!!currentMember.value &&
(isRejected(project.value) || isUnderReview(project.value) || isStaff(auth.value.user)),
shown: !!currentMember.value,
},
];
});

View File

@@ -76,8 +76,15 @@
<p>
This is a private conversation thread with the Modrinth moderators. They may message you
with issues concerning this project. This thread is only checked when you submit your
project for review. For additional inquiries, contact
<a href="https://support.modrinth.com">Modrinth Support</a>.
project for review. For additional inquiries, please go to the
<a class="text-link" href="https://support.modrinth.com" target="_blank">
Modrinth Help Center
</a>
and click the green bubble to contact support.
</p>
<p v-if="isApproved(project)" class="warning">
<IssuesIcon /> The moderators do not actively monitor this chat. However, they may still see
messages here if there is a problem with your project.
</p>
<ConversationThread
v-if="thread"

View File

@@ -58,6 +58,41 @@
</div>
</div>
</NewModal>
<NewModal ref="modifyModal">
<template #title>
<span class="text-lg font-extrabold text-contrast">Modify charge</span>
</template>
<div class="flex flex-col gap-3">
<div class="flex flex-col gap-2">
<label for="cancel" class="flex flex-col gap-1">
<span class="text-lg font-semibold text-contrast">
Cancel server
<span class="text-brand-red">*</span>
</span>
<span>
Whether or not the subscription should be cancelled. Submitting this as "true" will
cancel the subscription, while submitting it as "false" will force another charge
attempt to be made.
</span>
</label>
<Toggle id="cancel" v-model="cancel" />
</div>
<div class="flex gap-2">
<ButtonStyled color="brand">
<button :disabled="modifying" @click="modifyCharge">
<CheckIcon aria-hidden="true" />
Modify charge
</button>
</ButtonStyled>
<ButtonStyled>
<button @click="modifyModal.hide()">
<XIcon aria-hidden="true" />
Cancel
</button>
</ButtonStyled>
</div>
</div>
</NewModal>
<div class="page experimental-styles-within">
<div
class="mb-4 flex items-center justify-between border-0 border-b border-solid border-divider pb-4"
@@ -201,6 +236,12 @@
Refund options
</button>
</ButtonStyled>
<ButtonStyled v-else-if="charge.status === 'failed'" color="red" color-fill="text">
<button @click="showModifyModal(subscription)">
<CurrencyIcon />
Modify charge
</button>
</ButtonStyled>
</div>
</div>
</div>
@@ -234,7 +275,6 @@ import { products } from "~/generated/state.json";
import ModrinthServersIcon from "~/components/ui/servers/ModrinthServersIcon.vue";
const route = useRoute();
const data = useNuxtApp();
const vintl = useVIntl();
const { formatMessage } = vintl;
@@ -304,6 +344,10 @@ const refundTypes = ref(["full", "partial", "none"]);
const refundAmount = ref(0);
const unprovision = ref(true);
const modifying = ref(false);
const modifyModal = ref();
const cancel = ref(false);
function showRefundModal(charge) {
selectedCharge.value = charge;
refundType.value = "full";
@@ -312,6 +356,12 @@ function showRefundModal(charge) {
refundModal.value.show();
}
function showModifyModal(charge) {
selectedCharge.value = charge;
cancel.value = false;
modifyModal.value.show();
}
async function refundCharge() {
refunding.value = true;
try {
@@ -327,8 +377,7 @@ async function refundCharge() {
await refreshCharges();
refundModal.value.hide();
} catch (err) {
data.$notify({
group: "main",
addNotification({
title: "Error refunding",
text: err.data?.description ?? err,
type: "error",
@@ -337,6 +386,32 @@ async function refundCharge() {
refunding.value = false;
}
async function modifyCharge() {
modifying.value = true;
try {
await useBaseFetch(`billing/subscription/${selectedCharge.value.id}`, {
method: "PATCH",
body: JSON.stringify({
cancelled: cancel.value,
}),
internal: true,
});
addNotification({
title: "Resubscription request submitted",
text: "If the server is currently suspended, it may take up to 10 minutes for another charge attempt to be made.",
type: "success",
});
await refreshCharges();
} catch (err) {
addNotification({
title: "Error reattempting charge",
text: err.data?.description ?? err,
type: "error",
});
}
modifying.value = false;
}
const chargeStatuses = {
open: {
color: "bg-blue",

View File

@@ -40,7 +40,6 @@
@change="showPreviewImage"
>
<UploadIcon aria-hidden="true" />
{{ formatMessage(messages.uploadIconButton) }}
</FileInput>
<Button
v-if="!deletedIcon && (previewImage || collection.icon_url)"
@@ -479,10 +478,6 @@ const messages = defineMessages({
id: "collection.label.updated-at",
defaultMessage: "Updated {ago}",
},
uploadIconButton: {
id: "collection.button.upload-icon",
defaultMessage: "Upload icon",
},
});
const data = useNuxtApp();

View File

@@ -96,16 +96,7 @@
<UiServersServerListing
v-for="server in filteredData"
:key="server.server_id"
:server_id="server.server_id"
:name="server.name"
:status="server.status"
:game="server.game"
:loader="server.loader"
:loader_version="server.loader_version"
:mc_version="server.mc_version"
:upstream="server.upstream"
:net="server.net"
:flows="server.flows"
v-bind="server"
/>
<LazyUiServersServerListingSkeleton v-if="isPollingForNewServers" />
</ul>

View File

@@ -208,15 +208,7 @@
<div class="flex flex-col gap-2">
<UiServersServerListing
v-if="subscription.serverInfo"
:server_id="subscription.serverInfo.server_id"
:name="subscription.serverInfo.name"
:status="subscription.serverInfo.status"
:game="subscription.serverInfo.game"
:loader="subscription.serverInfo.loader"
:loader_version="subscription.serverInfo.loader_version"
:mc_version="subscription.serverInfo.mc_version"
:upstream="subscription.serverInfo.upstream"
:net="subscription.serverInfo.net"
v-bind="subscription.serverInfo"
/>
<div v-else class="w-fit">
<p>