You've already forked AstralRinth
forked from didirus/AstralRinth
Merge commit '175b90be5a42e5bfd3289ffdfbf7b201404f82a8' into beta
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
type ProtocolVersion,
|
||||
type ServerWorld,
|
||||
type ServerData,
|
||||
type WorldWithProfile,
|
||||
@@ -33,7 +34,7 @@ const theme = useTheming()
|
||||
|
||||
const jumpBackInItems = ref<JumpBackInItem[]>([])
|
||||
const serverData = ref<Record<string, ServerData>>({})
|
||||
const protocolVersions = ref<Record<string, number | null>>({})
|
||||
const protocolVersions = ref<Record<string, ProtocolVersion | null>>({})
|
||||
|
||||
const MIN_JUMP_BACK_IN = 3
|
||||
const MAX_JUMP_BACK_IN = 6
|
||||
@@ -121,11 +122,8 @@ async function populateJumpBackIn() {
|
||||
}
|
||||
})
|
||||
|
||||
// fetch each server's data
|
||||
Promise.all(
|
||||
servers.map(({ instancePath, address }) =>
|
||||
refreshServerData(serverData.value[address], protocolVersions.value[instancePath], address),
|
||||
),
|
||||
servers.forEach(({ instancePath, address }) =>
|
||||
refreshServerData(serverData.value[address], protocolVersions.value[instancePath], address),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -150,8 +148,8 @@ async function populateJumpBackIn() {
|
||||
.slice(0, MAX_JUMP_BACK_IN)
|
||||
}
|
||||
|
||||
async function refreshServer(address: string, instancePath: string) {
|
||||
await refreshServerData(serverData.value[address], protocolVersions.value[instancePath], address)
|
||||
function refreshServer(address: string, instancePath: string) {
|
||||
refreshServerData(serverData.value[address], protocolVersions.value[instancePath], address)
|
||||
}
|
||||
|
||||
async function joinWorld(world: WorldWithProfile) {
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import dayjs from 'dayjs'
|
||||
import type { ServerStatus, ServerWorld, SingleplayerWorld, World } from '@/helpers/worlds.ts'
|
||||
import { set_world_display_status, getWorldIdentifier } from '@/helpers/worlds.ts'
|
||||
import type {
|
||||
ProtocolVersion,
|
||||
ServerStatus,
|
||||
ServerWorld,
|
||||
SingleplayerWorld,
|
||||
World,
|
||||
set_world_display_status,
|
||||
getWorldIdentifier,
|
||||
} from '@/helpers/worlds.ts'
|
||||
import { formatNumber, getPingLevel } from '@modrinth/utils'
|
||||
import {
|
||||
useRelativeTime,
|
||||
@@ -55,7 +62,7 @@ const props = withDefaults(
|
||||
playingWorld?: boolean
|
||||
startingInstance?: boolean
|
||||
supportsQuickPlay?: boolean
|
||||
currentProtocol?: number | null
|
||||
currentProtocol?: ProtocolVersion | null
|
||||
highlighted?: boolean
|
||||
|
||||
// Server only
|
||||
@@ -102,7 +109,8 @@ const serverIncompatible = computed(
|
||||
!!props.serverStatus &&
|
||||
!!props.serverStatus.version?.protocol &&
|
||||
!!props.currentProtocol &&
|
||||
props.serverStatus.version.protocol !== props.currentProtocol,
|
||||
(props.serverStatus.version.protocol !== props.currentProtocol.version ||
|
||||
props.serverStatus.version.legacy !== props.currentProtocol.legacy),
|
||||
)
|
||||
|
||||
const locked = computed(() => props.world.type === 'singleplayer' && props.world.locked)
|
||||
|
||||
@@ -51,6 +51,7 @@ export type ServerStatus = {
|
||||
version?: {
|
||||
name: string
|
||||
protocol: number
|
||||
legacy: boolean
|
||||
}
|
||||
favicon?: string
|
||||
enforces_secure_chat: boolean
|
||||
@@ -70,11 +71,17 @@ export interface Chat {
|
||||
|
||||
export type ServerData = {
|
||||
refreshing: boolean
|
||||
lastSuccessfulRefresh?: number
|
||||
status?: ServerStatus
|
||||
rawMotd?: string | Chat
|
||||
renderedMotd?: string
|
||||
}
|
||||
|
||||
export type ProtocolVersion = {
|
||||
version: number
|
||||
legacy: boolean
|
||||
}
|
||||
|
||||
export async function get_recent_worlds(
|
||||
limit: number,
|
||||
displayStatuses?: DisplayStatus[],
|
||||
@@ -156,13 +163,13 @@ export async function remove_server_from_profile(path: string, index: number): P
|
||||
return await invoke('plugin:worlds|remove_server_from_profile', { path, index })
|
||||
}
|
||||
|
||||
export async function get_profile_protocol_version(path: string): Promise<number | null> {
|
||||
export async function get_profile_protocol_version(path: string): Promise<ProtocolVersion | null> {
|
||||
return await invoke('plugin:worlds|get_profile_protocol_version', { path })
|
||||
}
|
||||
|
||||
export async function get_server_status(
|
||||
address: string,
|
||||
protocolVersion: number | null = null,
|
||||
protocolVersion: ProtocolVersion | null = null,
|
||||
): Promise<ServerStatus> {
|
||||
return await invoke('plugin:worlds|get_server_status', { address, protocolVersion })
|
||||
}
|
||||
@@ -206,30 +213,39 @@ export function isServerWorld(world: World): world is ServerWorld {
|
||||
|
||||
export async function refreshServerData(
|
||||
serverData: ServerData,
|
||||
protocolVersion: number | null,
|
||||
protocolVersion: ProtocolVersion | null,
|
||||
address: string,
|
||||
): Promise<void> {
|
||||
const refreshTime = Date.now()
|
||||
serverData.refreshing = true
|
||||
await get_server_status(address, protocolVersion)
|
||||
.then((status) => {
|
||||
if (serverData.lastSuccessfulRefresh && serverData.lastSuccessfulRefresh > refreshTime) {
|
||||
// Don't update if there was a more recent successful refresh
|
||||
return
|
||||
}
|
||||
serverData.lastSuccessfulRefresh = Date.now()
|
||||
serverData.status = status
|
||||
if (status.description) {
|
||||
serverData.rawMotd = status.description
|
||||
serverData.renderedMotd = autoToHTML(status.description)
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(`Refreshing addr: ${address}`, err)
|
||||
})
|
||||
.finally(() => {
|
||||
serverData.refreshing = false
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(`Refreshing addr ${address}`, protocolVersion, err)
|
||||
if (!protocolVersion?.legacy) {
|
||||
refreshServerData(serverData, { version: 74, legacy: true }, address)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export async function refreshServers(
|
||||
export function refreshServers(
|
||||
worlds: World[],
|
||||
serverData: Record<string, ServerData>,
|
||||
protocolVersion: number | null,
|
||||
protocolVersion: ProtocolVersion | null,
|
||||
) {
|
||||
const servers = worlds.filter(isServerWorld)
|
||||
servers.forEach((server) => {
|
||||
@@ -243,10 +259,8 @@ export async function refreshServers(
|
||||
})
|
||||
|
||||
// noinspection ES6MissingAwait - handled with .then by refreshServerData already
|
||||
Promise.all(
|
||||
Object.keys(serverData).map((address) =>
|
||||
refreshServerData(serverData[address], protocolVersion, address),
|
||||
),
|
||||
Object.keys(serverData).forEach((address) =>
|
||||
refreshServerData(serverData[address], protocolVersion, address),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -134,6 +134,7 @@ import {
|
||||
} from '@modrinth/ui'
|
||||
import { PlusIcon, SpinnerIcon, UpdatedIcon, SearchIcon, XIcon } from '@modrinth/assets'
|
||||
import {
|
||||
type ProtocolVersion,
|
||||
type SingleplayerWorld,
|
||||
type World,
|
||||
type ServerWorld,
|
||||
@@ -210,7 +211,9 @@ const worldPlaying = ref<World>()
|
||||
const worlds = ref<World[]>([])
|
||||
const serverData = ref<Record<string, ServerData>>({})
|
||||
|
||||
const protocolVersion = ref<number | null>(await get_profile_protocol_version(instance.value.path))
|
||||
const protocolVersion = ref<ProtocolVersion | null>(
|
||||
await get_profile_protocol_version(instance.value.path),
|
||||
)
|
||||
|
||||
const unlistenProfile = await profile_listener(async (e: ProfileEvent) => {
|
||||
if (e.profile_path_id !== instance.value.path) return
|
||||
@@ -246,7 +249,7 @@ async function refreshAllWorlds() {
|
||||
worlds.value = await refreshWorlds(instance.value.path).finally(
|
||||
() => (refreshingAll.value = false),
|
||||
)
|
||||
await refreshServers(worlds.value, serverData.value, protocolVersion.value)
|
||||
refreshServers(worlds.value, serverData.value, protocolVersion.value)
|
||||
|
||||
const hasNoWorlds = worlds.value.length === 0
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@ use tauri::{AppHandle, Manager, Runtime};
|
||||
use theseus::prelude::ProcessMetadata;
|
||||
use theseus::profile::{QuickPlayType, get_full_path};
|
||||
use theseus::worlds::{
|
||||
DisplayStatus, ServerPackStatus, ServerStatus, World, WorldType,
|
||||
WorldWithProfile,
|
||||
DisplayStatus, ProtocolVersion, ServerPackStatus, ServerStatus, World,
|
||||
WorldType, WorldWithProfile,
|
||||
};
|
||||
use theseus::{profile, worlds};
|
||||
|
||||
@@ -183,14 +183,16 @@ pub async fn remove_server_from_profile(
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_profile_protocol_version(path: &str) -> Result<Option<i32>> {
|
||||
pub async fn get_profile_protocol_version(
|
||||
path: &str,
|
||||
) -> Result<Option<ProtocolVersion>> {
|
||||
Ok(worlds::get_profile_protocol_version(path).await?)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_server_status(
|
||||
address: &str,
|
||||
protocol_version: Option<i32>,
|
||||
protocol_version: Option<ProtocolVersion>,
|
||||
) -> Result<ServerStatus> {
|
||||
Ok(worlds::get_server_status(address, protocol_version).await?)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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."
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT id FROM reports\n WHERE closed = FALSE\n ORDER BY created ASC\n LIMIT $1;\n ",
|
||||
"query": "\n SELECT id FROM reports\n WHERE closed = FALSE\n ORDER BY created ASC\n OFFSET $2\n LIMIT $1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -11,6 +11,7 @@
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
@@ -18,5 +19,5 @@
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "29e171bd746ac5dc1fabae4c9f81c3d1df4e69c860b7d0f6a907377664199217"
|
||||
"hash": "1aea0d5e6936b043cb7727b779d60598aa812c8ef0f5895fa740859321092a1c"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT id FROM reports\n WHERE closed = FALSE AND reporter = $1\n ORDER BY created ASC\n LIMIT $2;\n ",
|
||||
"query": "\n SELECT id FROM reports\n WHERE closed = FALSE AND reporter = $1\n ORDER BY created ASC\n OFFSET $3\n LIMIT $2\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -11,6 +11,7 @@
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"Int8",
|
||||
"Int8"
|
||||
]
|
||||
@@ -19,5 +20,5 @@
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "f17a109913015a7a5ab847bb2e73794d6261a08d450de24b450222755e520881"
|
||||
"hash": "be8a5dd2b71fdc279a6fa68fe5384da31afd91d4b480527e2dd8402aef36f12c"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT id FROM mods\n WHERE status = $1\n ORDER BY queued ASC\n LIMIT $2;\n ",
|
||||
"query": "\n SELECT id FROM mods\n WHERE status = $1\n ORDER BY queued ASC\n OFFSET $3\n LIMIT $2\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -12,6 +12,7 @@
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Int8",
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
@@ -19,5 +20,5 @@
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "3baabc9f08401801fa290866888c540746fc50c1d79911f08f3322b605ce5c30"
|
||||
"hash": "ccb0315ff52ea4402f53508334a7288fc9f8e77ffd7bce665441ff682384cbf9"
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
CREATE INDEX reports_closed ON reports (closed);
|
||||
@@ -315,9 +315,13 @@ pub async fn filter_enlisted_version_ids(
|
||||
pub async fn is_visible_collection(
|
||||
collection_data: &DBCollection,
|
||||
user_option: &Option<User>,
|
||||
hide_unlisted: bool,
|
||||
) -> Result<bool, ApiError> {
|
||||
let mut authorized = !collection_data.status.is_hidden()
|
||||
&& !collection_data.projects.is_empty();
|
||||
let mut authorized = (if hide_unlisted {
|
||||
collection_data.status.is_searchable()
|
||||
} else {
|
||||
!collection_data.status.is_hidden()
|
||||
}) && !collection_data.projects.is_empty();
|
||||
if let Some(user) = &user_option {
|
||||
if !authorized
|
||||
&& (user.role.is_mod() || user.id == collection_data.user_id.into())
|
||||
@@ -331,12 +335,17 @@ pub async fn is_visible_collection(
|
||||
pub async fn filter_visible_collections(
|
||||
collections: Vec<DBCollection>,
|
||||
user_option: &Option<User>,
|
||||
hide_unlisted: bool,
|
||||
) -> Result<Vec<crate::models::collections::Collection>, ApiError> {
|
||||
let mut return_collections = Vec::new();
|
||||
let mut check_collections = Vec::new();
|
||||
|
||||
for collection in collections {
|
||||
if (!collection.status.is_hidden() && !collection.projects.is_empty())
|
||||
if ((if hide_unlisted {
|
||||
collection.status.is_searchable()
|
||||
} else {
|
||||
!collection.status.is_hidden()
|
||||
}) && !collection.projects.is_empty())
|
||||
|| user_option.as_ref().is_some_and(|x| x.role.is_mod())
|
||||
{
|
||||
return_collections.push(collection.into());
|
||||
|
||||
@@ -92,7 +92,7 @@ impl CollectionStatus {
|
||||
}
|
||||
}
|
||||
|
||||
// Project pages + info cannot be viewed
|
||||
// Collection pages + info cannot be viewed
|
||||
pub fn is_hidden(&self) -> bool {
|
||||
match self {
|
||||
CollectionStatus::Rejected => true,
|
||||
@@ -103,6 +103,11 @@ impl CollectionStatus {
|
||||
}
|
||||
}
|
||||
|
||||
// Collection can be displayed in on user page
|
||||
pub fn is_searchable(&self) -> bool {
|
||||
matches!(self, CollectionStatus::Listed)
|
||||
}
|
||||
|
||||
pub fn is_approved(&self) -> bool {
|
||||
match self {
|
||||
CollectionStatus::Listed => true,
|
||||
|
||||
@@ -276,7 +276,11 @@ pub async fn refund_charge(
|
||||
subscription_interval: charge.subscription_interval,
|
||||
payment_platform: charge.payment_platform,
|
||||
payment_platform_id: id,
|
||||
parent_charge_id: Some(charge.id),
|
||||
parent_charge_id: if refund_amount != 0 {
|
||||
Some(charge.id)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
net,
|
||||
}
|
||||
.upsert(&mut transaction)
|
||||
|
||||
@@ -18,12 +18,14 @@ pub fn config(cfg: &mut web::ServiceConfig) {
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct ResultCount {
|
||||
pub struct ProjectsRequestOptions {
|
||||
#[serde(default = "default_count")]
|
||||
pub count: i16,
|
||||
pub count: u16,
|
||||
#[serde(default)]
|
||||
pub offset: u32,
|
||||
}
|
||||
|
||||
fn default_count() -> i16 {
|
||||
fn default_count() -> u16 {
|
||||
100
|
||||
}
|
||||
|
||||
@@ -31,7 +33,7 @@ pub async fn get_projects(
|
||||
req: HttpRequest,
|
||||
pool: web::Data<PgPool>,
|
||||
redis: web::Data<RedisPool>,
|
||||
count: web::Query<ResultCount>,
|
||||
request_opts: web::Query<ProjectsRequestOptions>,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
check_is_moderator_from_headers(
|
||||
@@ -50,10 +52,12 @@ pub async fn get_projects(
|
||||
SELECT id FROM mods
|
||||
WHERE status = $1
|
||||
ORDER BY queued ASC
|
||||
LIMIT $2;
|
||||
OFFSET $3
|
||||
LIMIT $2
|
||||
",
|
||||
ProjectStatus::Processing.as_str(),
|
||||
count.count as i64
|
||||
request_opts.count as i64,
|
||||
request_opts.offset as i64
|
||||
)
|
||||
.fetch(&**pool)
|
||||
.map_ok(|m| database::models::DBProjectId(m.id))
|
||||
|
||||
@@ -15,10 +15,10 @@ pub fn config(cfg: &mut web::ServiceConfig) {
|
||||
#[derive(Deserialize)]
|
||||
pub struct ResultCount {
|
||||
#[serde(default = "default_count")]
|
||||
pub count: i16,
|
||||
pub count: u16,
|
||||
}
|
||||
|
||||
fn default_count() -> i16 {
|
||||
fn default_count() -> u16 {
|
||||
100
|
||||
}
|
||||
|
||||
@@ -34,7 +34,10 @@ pub async fn get_projects(
|
||||
req,
|
||||
pool.clone(),
|
||||
redis.clone(),
|
||||
web::Query(internal::moderation::ResultCount { count: count.count }),
|
||||
web::Query(internal::moderation::ProjectsRequestOptions {
|
||||
count: count.count,
|
||||
offset: 0,
|
||||
}),
|
||||
session_queue,
|
||||
)
|
||||
.await
|
||||
|
||||
@@ -43,12 +43,12 @@ pub async fn report_create(
|
||||
#[derive(Deserialize)]
|
||||
pub struct ReportsRequestOptions {
|
||||
#[serde(default = "default_count")]
|
||||
count: i16,
|
||||
count: u16,
|
||||
#[serde(default = "default_all")]
|
||||
all: bool,
|
||||
}
|
||||
|
||||
fn default_count() -> i16 {
|
||||
fn default_count() -> u16 {
|
||||
100
|
||||
}
|
||||
fn default_all() -> bool {
|
||||
@@ -60,7 +60,7 @@ pub async fn reports(
|
||||
req: HttpRequest,
|
||||
pool: web::Data<PgPool>,
|
||||
redis: web::Data<RedisPool>,
|
||||
count: web::Query<ReportsRequestOptions>,
|
||||
request_opts: web::Query<ReportsRequestOptions>,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let response = v3::reports::reports(
|
||||
@@ -68,8 +68,9 @@ pub async fn reports(
|
||||
pool,
|
||||
redis,
|
||||
web::Query(v3::reports::ReportsRequestOptions {
|
||||
count: count.count,
|
||||
all: count.all,
|
||||
count: request_opts.count,
|
||||
offset: 0,
|
||||
all: request_opts.all,
|
||||
}),
|
||||
session_queue,
|
||||
)
|
||||
|
||||
@@ -163,7 +163,8 @@ pub async fn collections_get(
|
||||
.ok();
|
||||
|
||||
let collections =
|
||||
filter_visible_collections(collections_data, &user_option).await?;
|
||||
filter_visible_collections(collections_data, &user_option, false)
|
||||
.await?;
|
||||
|
||||
Ok(HttpResponse::Ok().json(collections))
|
||||
}
|
||||
@@ -192,7 +193,7 @@ pub async fn collection_get(
|
||||
.ok();
|
||||
|
||||
if let Some(data) = collection_data {
|
||||
if is_visible_collection(&data, &user_option).await? {
|
||||
if is_visible_collection(&data, &user_option, false).await? {
|
||||
return Ok(HttpResponse::Ok().json(Collection::from(data)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -222,12 +222,14 @@ pub async fn report_create(
|
||||
#[derive(Deserialize)]
|
||||
pub struct ReportsRequestOptions {
|
||||
#[serde(default = "default_count")]
|
||||
pub count: i16,
|
||||
pub count: u16,
|
||||
#[serde(default)]
|
||||
pub offset: u32,
|
||||
#[serde(default = "default_all")]
|
||||
pub all: bool,
|
||||
}
|
||||
|
||||
fn default_count() -> i16 {
|
||||
fn default_count() -> u16 {
|
||||
100
|
||||
}
|
||||
fn default_all() -> bool {
|
||||
@@ -238,7 +240,7 @@ pub async fn reports(
|
||||
req: HttpRequest,
|
||||
pool: web::Data<PgPool>,
|
||||
redis: web::Data<RedisPool>,
|
||||
count: web::Query<ReportsRequestOptions>,
|
||||
request_opts: web::Query<ReportsRequestOptions>,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let user = get_user_from_headers(
|
||||
@@ -253,15 +255,17 @@ pub async fn reports(
|
||||
|
||||
use futures::stream::TryStreamExt;
|
||||
|
||||
let report_ids = if user.role.is_mod() && count.all {
|
||||
let report_ids = if user.role.is_mod() && request_opts.all {
|
||||
sqlx::query!(
|
||||
"
|
||||
SELECT id FROM reports
|
||||
WHERE closed = FALSE
|
||||
ORDER BY created ASC
|
||||
LIMIT $1;
|
||||
OFFSET $2
|
||||
LIMIT $1
|
||||
",
|
||||
count.count as i64
|
||||
request_opts.count as i64,
|
||||
request_opts.offset as i64
|
||||
)
|
||||
.fetch(&**pool)
|
||||
.map_ok(|m| crate::database::models::ids::DBReportId(m.id))
|
||||
@@ -273,10 +277,12 @@ pub async fn reports(
|
||||
SELECT id FROM reports
|
||||
WHERE closed = FALSE AND reporter = $1
|
||||
ORDER BY created ASC
|
||||
LIMIT $2;
|
||||
OFFSET $3
|
||||
LIMIT $2
|
||||
",
|
||||
user.id.0 as i64,
|
||||
count.count as i64
|
||||
request_opts.count as i64,
|
||||
request_opts.offset as i64
|
||||
)
|
||||
.fetch(&**pool)
|
||||
.map_ok(|m| crate::database::models::ids::DBReportId(m.id))
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
use super::{ApiError, oauth_clients::get_user_clients};
|
||||
use crate::file_hosting::FileHostPublicity;
|
||||
use crate::util::img::delete_old_images;
|
||||
use crate::{
|
||||
auth::{filter_visible_projects, get_user_from_headers},
|
||||
auth::{
|
||||
filter_visible_collections, filter_visible_projects,
|
||||
get_user_from_headers,
|
||||
},
|
||||
database::{models::DBUser, redis::RedisPool},
|
||||
file_hosting::FileHost,
|
||||
file_hosting::{FileHost, FileHostPublicity},
|
||||
models::{
|
||||
collections::{Collection, CollectionStatus},
|
||||
notifications::Notification,
|
||||
pats::Scopes,
|
||||
projects::Project,
|
||||
@@ -16,7 +16,7 @@ use crate::{
|
||||
},
|
||||
queue::session::AuthQueue,
|
||||
util::{
|
||||
routes::read_limited_from_payload,
|
||||
img::delete_old_images, routes::read_limited_from_payload,
|
||||
validate::validation_errors_to_string,
|
||||
},
|
||||
};
|
||||
@@ -244,27 +244,19 @@ pub async fn collections_list(
|
||||
let id_option = DBUser::get(&info.into_inner().0, &**pool, &redis).await?;
|
||||
|
||||
if let Some(id) = id_option.map(|x| x.id) {
|
||||
let user_id: UserId = id.into();
|
||||
|
||||
let can_view_private =
|
||||
user.is_some_and(|y| y.role.is_mod() || y.id == user_id);
|
||||
|
||||
let project_data = DBUser::get_collections(id, &**pool).await?;
|
||||
let collection_data = DBUser::get_collections(id, &**pool).await?;
|
||||
|
||||
let response: Vec<_> = crate::database::models::DBCollection::get_many(
|
||||
&project_data,
|
||||
&collection_data,
|
||||
&**pool,
|
||||
&redis,
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
.filter(|x| {
|
||||
can_view_private || matches!(x.status, CollectionStatus::Listed)
|
||||
})
|
||||
.map(Collection::from)
|
||||
.collect();
|
||||
.await?;
|
||||
|
||||
Ok(HttpResponse::Ok().json(response))
|
||||
let collections =
|
||||
filter_visible_collections(response, &user, true).await?;
|
||||
|
||||
Ok(HttpResponse::Ok().json(collections))
|
||||
} else {
|
||||
Err(ApiError::NotFound)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user