Threads and more! (#1232)

* Begin UI for threads and moderation overhaul

* Hide close button on non-report threads

* Fix review age coloring

* Add project count

* Remove action buttons from queue page and add queued date to project page

* Hook up to actual data

* Remove unused icon

* Get up to 1000 projects in queue

* prettier

* more prettier

* Changed all the things

* lint

* rebuild

* Add omorphia

* Workaround formatjs bug in ThreadSummary.vue

* Fix notifications page on prod

* Fix a few notifications and threads bugs

* lockfile

* Fix duplicate button styles

* more fixes and polishing

* More fixes

* Remove legacy pages

* More bugfixes

* Add some error catching for reports and notifications

* More error handling

* fix lint

* Add inbox links

* Remove loading component and rename member header

* Rely on threads always existing

* Handle if project update notifs are not grouped

* oops

* Fix chips on notifications page

* Import ModalModeration

* finish threads

---------

Co-authored-by: triphora <emma@modrinth.com>
Co-authored-by: Jai A <jaiagr+gpg@pm.me>
This commit is contained in:
Prospector
2023-07-15 20:39:33 -07:00
committed by GitHub
parent 1a2b45eebd
commit a5613ebb10
67 changed files with 3613 additions and 776 deletions

View File

@@ -94,7 +94,7 @@
</aside>
</div>
<div class="normal-page__content">
<ProjectPublishingChecklist
<ProjectMemberHeader
v-if="currentMember"
:project="project"
:versions="versions"
@@ -104,6 +104,8 @@
:set-processing="setProcessing"
:collapsed="collapsedChecklist"
:toggle-collapsed="() => (collapsedChecklist = !collapsedChecklist)"
:all-members="allMembers"
:update-members="updateMembers"
/>
<NuxtPage
v-model:project="project"
@@ -258,7 +260,7 @@
</div>
<div class="dates">
<div
v-tooltip="$dayjs(project.published).format('MMMM D, YYYY [at] h:mm:ss A')"
v-tooltip="$dayjs(project.published).format('MMMM D, YYYY [at] h:mm A')"
class="date"
>
<CalendarIcon aria-hidden="true" />
@@ -266,13 +268,22 @@
<span class="value">{{ fromNow(project.published) }}</span>
</div>
<div
v-tooltip="$dayjs(project.updated).format('MMMM D, YYYY [at] h:mm:ss A')"
v-tooltip="$dayjs(project.updated).format('MMMM D, YYYY [at] h:mm A')"
class="date"
>
<UpdateIcon aria-hidden="true" />
<span class="label">Updated</span>
<span class="value">{{ fromNow(project.updated) }}</span>
</div>
<div
v-if="project.status === 'processing' && project.queued"
v-tooltip="$dayjs(project.queued).format('MMMM D, YYYY [at] h:mm A')"
class="date"
>
<QueuedIcon aria-hidden="true" />
<span class="label">Submitted</span>
<span class="value">{{ fromNow(project.queued) }}</span>
</div>
</div>
<hr class="card-divider" />
<div class="input-group">
@@ -361,17 +372,21 @@
</button>
<button
v-if="
$tag.approvedStatuses.includes(project.status) || project.status === 'processing'
$tag.approvedStatuses.includes(project.status) ||
project.status === 'processing' ||
($tag.rejectedStatuses.includes(project.status) && project.status !== 'withheld')
"
class="iconified-button danger-button"
@click="openModerationModal('withheld')"
>
<EyeIcon />
<EyeOffIcon />
Withhold
</button>
<button
v-if="
$tag.approvedStatuses.includes(project.status) || project.status === 'processing'
$tag.approvedStatuses.includes(project.status) ||
project.status === 'processing' ||
($tag.rejectedStatuses.includes(project.status) && project.status !== 'rejected')
"
class="iconified-button danger-button"
@click="openModerationModal('rejected')"
@@ -383,15 +398,19 @@
<EditIcon />
Edit message
</button>
<nuxt-link class="iconified-button" to="/moderation">
<nuxt-link class="iconified-button" to="/moderation/review">
<ModerationIcon />
Visit moderation queue
Visit review queue
</nuxt-link>
<nuxt-link class="iconified-button" to="/moderation/reports">
<ReportIcon />
Visit reports
</nuxt-link>
</div>
</div>
</div>
<section class="normal-page__content">
<ProjectPublishingChecklist
<ProjectMemberHeader
v-if="currentMember"
:project="project"
:versions="versions"
@@ -401,6 +420,8 @@
:set-processing="setProcessing"
:collapsed="collapsedChecklist"
:toggle-collapsed="() => (collapsedChecklist = !collapsedChecklist)"
:all-members="allMembers"
:update-members="updateMembers"
/>
<div v-else-if="project.status === 'withheld'" class="card warning" aria-label="Warning">
{{ project.title }} has been removed from search by Modrinth's moderators. Please use
@@ -455,6 +476,13 @@
}/versions`,
shown: versions.length > 0 || !!currentMember,
},
{
label: 'Moderation',
href: `/${project.project_type}/${
project.slug ? project.slug : project.id
}/moderation`,
shown: !!currentMember,
},
]"
/>
<div v-if="$auth.user && currentMember" class="input-group">
@@ -689,6 +717,38 @@
<CopyCode :text="project.id" />
</div>
</div>
<div class="input-group">
<a
v-if="
config.public.apiBaseUrl.startsWith('https://api.modrinth.com') &&
config.public.siteUrl !== 'https://modrinth.com'
"
class="iconified-button"
:href="`https://modrinth.com/${project.project_type}/${
project.slug ? project.slug : project.id
}`"
rel="noopener nofollow"
target="_blank"
>
<ExternalIcon aria-hidden="true" />
View on modrinth.com
</a>
<a
v-else-if="
config.public.apiBaseUrl.startsWith('https://staging-api.modrinth.com') &&
config.public.siteUrl !== 'https://staging.modrinth.com'
"
class="iconified-button"
:href="`https://staging.modrinth.com/${project.project_type}/${
project.slug ? project.slug : project.id
}`"
rel="noopener nofollow"
target="_blank"
>
<ExternalIcon aria-hidden="true" />
View on staging.modrinth.com
</a>
</div>
</div>
</div>
</div>
@@ -700,7 +760,9 @@ import CheckIcon from '~/assets/images/utils/check.svg'
import ClearIcon from '~/assets/images/utils/clear.svg'
import DownloadIcon from '~/assets/images/utils/download.svg'
import UpdateIcon from '~/assets/images/utils/updated.svg'
import QueuedIcon from '~/assets/images/utils/list-end.svg'
import CodeIcon from '~/assets/images/sidebar/mod.svg'
import ExternalIcon from '~/assets/images/utils/external.svg'
import ReportIcon from '~/assets/images/utils/report.svg'
import HeartIcon from '~/assets/images/utils/heart.svg'
import IssuesIcon from '~/assets/images/utils/issues.svg'
@@ -713,7 +775,7 @@ import PayPalIcon from '~/assets/images/external/paypal.svg'
import OpenCollectiveIcon from '~/assets/images/external/opencollective.svg'
import UnknownIcon from '~/assets/images/utils/unknown-donation.svg'
import ChevronRightIcon from '~/assets/images/utils/chevron-right.svg'
import EyeIcon from '~/assets/images/utils/eye.svg'
import EyeOffIcon from '~/assets/images/utils/eye-off.svg'
import BoxIcon from '~/assets/images/utils/box.svg'
import Promotion from '~/components/ads/Promotion.vue'
import Badge from '~/components/ui/Badge.vue'
@@ -727,7 +789,7 @@ import CopyCode from '~/components/ui/CopyCode.vue'
import Avatar from '~/components/ui/Avatar.vue'
import NavStack from '~/components/ui/NavStack.vue'
import NavStackItem from '~/components/ui/NavStackItem.vue'
import ProjectPublishingChecklist from '~/components/ui/ProjectPublishingChecklist.vue'
import ProjectMemberHeader from '~/components/ui/ProjectMemberHeader.vue'
import SettingsIcon from '~/assets/images/utils/settings.svg'
import UsersIcon from '~/assets/images/utils/users.svg'
import CategoriesIcon from '~/assets/images/utils/tags.svg'
@@ -744,6 +806,7 @@ import Breadcrumbs from '~/components/ui/Breadcrumbs.vue'
const data = useNuxtApp()
const route = useRoute()
const config = useRuntimeConfig()
const user = await useUser()
@@ -1070,6 +1133,23 @@ function openModerationModal(status) {
modalModeration.value.show()
}
async function updateMembers() {
allMembers.value = await useAsyncData(
`project/${route.params.id}/members`,
() => useBaseFetch(`project/${route.params.id}/members`, data.$defaultHeaders()),
{
transform: (members) => {
members.forEach((it, index) => {
members[index].avatar_url = it.user.avatar_url
members[index].name = it.user.username
})
return members
},
}
)
}
const collapsedChecklist = ref(false)
</script>
<style lang="scss" scoped>

View File

@@ -0,0 +1,193 @@
<template>
<div>
<section class="universal-card">
<h2>Project status</h2>
<Badge :type="project.status" />
<p v-if="isApproved(project)">
Your project been approved by the moderators and you may freely change project visibility in
<router-link :to="`${getProjectLink(project)}/settings`" class="text-link"
>your project's settings</router-link
>.
</p>
<p v-else-if="isUnderReview(project)">
Project reviews typically take 24 to 48 hours and they will leave a message below if they
have any questions or concerns for you. If your review has taken more than 48 hours, check
our Discord or social media for moderation delays.
</p>
<template v-else-if="isRejected(project)">
<p>
Your project does not currently meet Modrinth's
<nuxt-link to="/legal/rules" class="text-link" target="_blank">content rules</nuxt-link>
and the moderators have requested you make changes before it can be approved. Read the
messages from the moderators below and address their comments before resubmitting.
</p>
<p class="warning">
Repeated submissions without addressing the moderators' comments may result in an account
suspension.
</p>
</template>
<h3>Current visibility</h3>
<ul class="visibility-info">
<li v-if="isListed(project)">
<CheckIcon class="good" />
Listed in search results
</li>
<li v-else>
<ExitIcon class="bad" />
Not listed in search results
</li>
<li v-if="isListed(project)">
<CheckIcon class="good" />
Listed on the profiles of members
</li>
<li v-else>
<ExitIcon class="bad" />
Not listed on the profiles of members
</li>
<li v-if="isPrivate(project)">
<ExitIcon class="bad" />
Not accessible with a direct link
</li>
<li v-else>
<CheckIcon class="good" />
Accessible with a direct link
</li>
</ul>
</section>
<section id="messages" class="universal-card">
<h2>Messages</h2>
<p>
This is a private conversation thread with the Modrinth moderators. They will message you
for issues concerning your project on Modrinth, and you are welcome to message them about
things concerning your project.
</p>
<ConversationThread
v-if="thread"
:thread="thread"
:update-thread="(newThread) => (thread = newThread)"
:project="project"
:set-status="setStatus"
:current-member="currentMember"
/>
</section>
</div>
</template>
<script setup>
import ConversationThread from '~/components/ui/thread/ConversationThread.vue'
import Badge from '~/components/ui/Badge.vue'
import {
getProjectLink,
isApproved,
isListed,
isPrivate,
isRejected,
isUnderReview,
} from '~/helpers/projects.js'
import ExitIcon from 'assets/images/utils/x.svg'
import CheckIcon from 'assets/images/utils/check.svg'
const props = defineProps({
project: {
type: Object,
default() {
return {}
},
},
currentMember: {
type: Object,
default() {
return null
},
},
})
const emit = defineEmits(['update:project'])
const app = useNuxtApp()
const { data: thread } = await useAsyncData(`thread/${props.project.thread_id}`, () =>
useBaseFetch(`thread/${props.project.thread_id}`, app.$defaultHeaders())
)
async function setStatus(status) {
startLoading()
try {
const data = {}
data.status = status
await useBaseFetch(`project/${props.project.id}`, {
method: 'PATCH',
body: data,
...app.$defaultHeaders(),
})
const project = props.project
project.status = status
emit('update:project', project)
thread.value = await useBaseFetch(`thread/${thread.value.id}`, app.$defaultHeaders())
} catch (err) {
app.$notify({
group: 'main',
title: 'An error occurred',
text: err.data ? err.data.description : err,
type: 'error',
})
}
stopLoading()
}
</script>
<style lang="scss" scoped>
.stacked {
display: flex;
flex-direction: column;
}
.status-message {
:deep(.badge) {
display: contents;
svg {
vertical-align: top;
margin: 0;
}
}
p:last-child {
margin-bottom: 0;
}
}
.unavailable-error {
.code {
margin-top: var(--spacing-card-sm);
}
svg {
vertical-align: top;
}
}
.visibility-info {
padding: 0;
list-style: none;
li {
display: flex;
align-items: center;
gap: var(--spacing-card-xs);
}
}
svg {
&.good {
color: var(--color-brand-green);
}
&.bad {
color: var(--color-special-red);
}
}
.warning {
color: var(--color-special-orange);
}
</style>

View File

@@ -147,10 +147,12 @@
<div class="adjacent-input">
<label for="project-visibility">
<span class="label__title">Visibility</span>
<span class="label__description">
<div class="label__description">
Listed and archived projects are visible in search. Unlisted projects are published, but
not visible in search or on user profiles. Private projects are only accessible by
members of the project.
<p>If approved by the moderators:</p>
<ul class="visibility-info">
<li>
<CheckIcon
@@ -183,7 +185,7 @@
{{ hasModifiedVisibility() ? 'Will be v' : 'V' }}isible via URL
</li>
</ul>
</span>
</div>
</label>
<Multiselect
id="project-visibility"
@@ -408,7 +410,7 @@ export default defineNuxtComponent({
...this.$defaultHeaders(),
})
await initUserProjects()
await this.$router.push('/dashboard/projects')
await this.$router.push('/dashboard/review')
this.$notify({
group: 'main',
title: 'Project deleted',

View File

@@ -13,17 +13,39 @@
project.
</span>
</span>
<div
v-if="(currentMember.permissions & MANAGE_INVITES) === MANAGE_INVITES"
class="input-group"
>
<input id="username" v-model="currentUsername" type="text" placeholder="Username" />
<div class="input-group">
<input
id="username"
v-model="currentUsername"
type="text"
placeholder="Username"
:disabled="(currentMember.permissions & MANAGE_INVITES) !== MANAGE_INVITES"
@keypress.enter="inviteTeamMember()"
/>
<label for="username" class="hidden">Username</label>
<button class="iconified-button brand-button" @click="inviteTeamMember">
<button
class="iconified-button brand-button"
:disabled="(currentMember.permissions & MANAGE_INVITES) !== MANAGE_INVITES"
@click="inviteTeamMember()"
>
<UserPlusIcon />
Invite
</button>
</div>
<div class="adjacent-input">
<span class="label">
<span class="label__title">Leave project</span>
<span class="label__description"> Remove yourself as a member of this project. </span>
</span>
<button
class="iconified-button danger-button"
:disabled="currentMember.role === 'Owner'"
@click="leaveProject()"
>
<UserRemoveIcon />
Leave project
</button>
</div>
</div>
<div
v-for="(member, index) in allTeamMembers"
@@ -227,6 +249,7 @@ import TransferIcon from '~/assets/images/utils/transfer.svg'
import UserPlusIcon from '~/assets/images/utils/user-plus.svg'
import UserRemoveIcon from '~/assets/images/utils/user-x.svg'
import Avatar from '~/components/ui/Avatar.vue'
import { removeSelfFromTeam } from '~/helpers/teams.js'
export default defineNuxtComponent({
components: {
@@ -282,6 +305,11 @@ export default defineNuxtComponent({
this.VIEW_PAYOUTS = 1 << 9
},
methods: {
removeSelfFromTeam,
async leaveProject() {
await removeSelfFromTeam(project.team)
await this.$router.push('/dashboard/projects')
},
async inviteTeamMember() {
startLoading()
@@ -297,6 +325,7 @@ export default defineNuxtComponent({
body: data,
...this.$defaultHeaders(),
})
this.currentUsername = ''
await this.updateMembers()
} catch (err) {
this.$notify({

View File

@@ -135,8 +135,8 @@
</button>
<button class="iconified-button" @click="version.featured = !version.featured">
<StarIcon aria-hidden="true" />
<template v-if="!version.featured"> Feature version </template>
<template v-else> Unfeature version </template>
<template v-if="!version.featured"> Feature version</template>
<template v-else> Unfeature version</template>
</button>
<nuxt-link
v-if="currentMember"
@@ -160,11 +160,7 @@
<DownloadIcon aria-hidden="true" />
Download
</a>
<button
v-if="$auth.user && !currentMember"
class="iconified-button"
@click="$refs.modal_version_report.show()"
>
<button class="iconified-button" @click="$refs.modal_version_report.show()">
<ReportIcon aria-hidden="true" />
Report
</button>
@@ -240,12 +236,12 @@
/>
</div>
<div
v-if="version.dependencies.length > 0 || (isEditing && project.project_type !== 'modpack')"
v-if="deps.length > 0 || (isEditing && project.project_type !== 'modpack')"
class="version-page__dependencies universal-card"
>
<h3>Dependencies</h3>
<div
v-for="(dependency, index) in version.dependencies.filter((x) => !x.file_name)"
v-for="(dependency, index) in deps.filter((x) => !x.file_name)"
:key="index"
class="dependency"
:class="{ 'button-transparent': !isEditing }"
@@ -260,11 +256,11 @@
<span class="project-title">
{{ dependency.project ? dependency.project.title : 'Unknown Project' }}
</span>
<span v-if="dependency.version">
<span v-if="dependency.version" class="dep-type" :class="dependency.dependency_type">
Version {{ dependency.version.version_number }} is
{{ dependency.dependency_type }}
</span>
<span v-else class="dep-type">
<span v-else class="dep-type" :class="dependency.dependency_type">
{{ dependency.dependency_type }}
</span>
</nuxt-link>
@@ -272,11 +268,11 @@
<span class="project-title">
{{ dependency.project ? dependency.project.title : 'Unknown Project' }}
</span>
<span v-if="dependency.version">
<span v-if="dependency.version" class="dep-type" :class="dependency.dependency_type">
Version {{ dependency.version.version_number }} is
{{ dependency.dependency_type }}
</span>
<span v-else class="dep-type">
<span v-else class="dep-type" :class="dependency.dependency_type">
{{ dependency.dependency_type }}
</span>
</div>
@@ -290,7 +286,7 @@
</button>
</div>
<div
v-for="(dependency, index) in version.dependencies.filter((x) => x.file_name)"
v-for="(dependency, index) in deps.filter((x) => x.file_name)"
:key="index"
class="dependency"
>
@@ -299,7 +295,7 @@
<span class="project-title">
{{ dependency.file_name }}
</span>
<span>Added via overrides</span>
<span class="dep-type" :class="dependency.dependency_type">Added via overrides</span>
</div>
</div>
<div v-if="isEditing && project.project_type !== 'modpack'" class="add-dependency">
@@ -630,7 +626,7 @@
<div v-if="!isEditing">
<h4>Publication date</h4>
<span>
{{ $dayjs(version.date_published).format('MMMM D, YYYY [at] h:mm:ss A') }}
{{ $dayjs(version.date_published).format('MMMM D, YYYY [at] h:mm A') }}
</span>
</div>
<div v-if="!isEditing && version.author">
@@ -896,6 +892,8 @@ export default defineNuxtComponent({
oldFileTypes = version.files.map((x) => fileTypes.find((y) => y.value === x.file_type))
const order = ['required', 'optional', 'incompatible', 'embedded']
return {
fileTypes: ref(fileTypes),
oldFileTypes: ref(oldFileTypes),
@@ -919,6 +917,11 @@ export default defineNuxtComponent({
.$dayjs(version.date_published)
.format('MMM D, YYYY')}. ${version.downloads} downloads.`
),
deps: computed(() =>
version.dependencies.sort(
(a, b) => order.indexOf(a.dependency_type) - order.indexOf(b.dependency_type)
)
),
}
},
data() {
@@ -1428,6 +1431,11 @@ export default defineNuxtComponent({
.dep-type {
text-transform: capitalize;
color: var(--color-text-secondary);
&.incompatible {
color: var(--color-red);
}
}
}
@@ -1529,6 +1537,7 @@ export default defineNuxtComponent({
h4 {
margin-bottom: 0.5rem;
}
label {
margin-top: 0.5rem;
}

View File

@@ -290,10 +290,6 @@ async function handleFiles(files) {
flex-direction: column;
gap: var(--spacing-card-xs);
}
&:active:not(&:disabled) {
transform: scale(0.99) !important;
}
}
}