Add private mod notes and message deletion (#1494)

* Really simply private messages impl

* Make private notes better

* Add thread message deletion

* Lint
This commit is contained in:
Prospector
2024-01-09 13:32:38 -08:00
committed by GitHub
parent 660cf0c1e4
commit a3b77bb37f
5 changed files with 127 additions and 70 deletions

View File

@@ -14,8 +14,8 @@
<ConversationThread
:thread="thread"
:report="report"
:update-thread="updateThread"
:auth="auth"
@update-thread="updateThread"
/>
</section>
</div>

View File

@@ -44,7 +44,9 @@
:message="message"
:members="members"
:report="report"
:auth="auth"
raised
@update-thread="() => updateThreadLocal()"
/>
</div>
<span v-if="report && report.closed">
@@ -61,20 +63,23 @@
<div class="input-group">
<button
v-if="sortedMessages.length > 0"
class="iconified-button brand-button"
class="btn btn-primary"
:disabled="!replyBody"
@click="sendReply()"
>
<ReplyIcon /> Reply
</button>
<button
v-else
class="iconified-button brand-button"
:disabled="!replyBody"
@click="sendReply()"
>
<button v-else class="btn btn-primary" :disabled="!replyBody" @click="sendReply()">
<SendIcon /> Send
</button>
<button
v-if="isStaff(auth.user)"
class="btn"
:disabled="!replyBody"
@click="sendReply(null, true)"
>
<ModerationIcon /> Add private note
</button>
<template v-if="currentMember && !isStaff(auth.user)">
<template v-if="isRejected(project)">
<button
@@ -113,7 +118,7 @@
<template v-if="isStaff(auth.user)">
<button
v-if="replyBody"
class="iconified-button brand-button"
class="btn btn-green"
:disabled="isApproved(project)"
@click="sendReply(requestedStatus)"
>
@@ -121,44 +126,64 @@
</button>
<button
v-else
class="iconified-button brand-button"
class="btn btn-green"
:disabled="isApproved(project)"
@click="setStatus(requestedStatus)"
>
<CheckIcon /> Approve project
</button>
<button
v-if="replyBody"
class="iconified-button moderation-button"
:disabled="project.status === 'withheld'"
@click="sendReply('withheld')"
>
<EyeOffIcon /> Withhold with reply
</button>
<button
v-else
class="iconified-button moderation-button"
:disabled="project.status === 'withheld'"
@click="setStatus('withheld')"
>
<EyeOffIcon /> Withhold project
</button>
<button
v-if="replyBody"
class="iconified-button danger-button"
:disabled="project.status === 'rejected'"
@click="sendReply('rejected')"
>
<CrossIcon /> Reject with reply
</button>
<button
v-else
class="iconified-button danger-button"
:disabled="project.status === 'rejected'"
@click="setStatus('rejected')"
>
<CrossIcon /> Reject project
<CheckIcon /> Approve
</button>
<div class="joined-buttons">
<button
v-if="replyBody"
class="btn btn-danger"
:disabled="project.status === 'rejected'"
@click="sendReply('rejected')"
>
<CrossIcon /> Reject with reply
</button>
<button
v-else
class="btn btn-danger"
:disabled="project.status === 'rejected'"
@click="setStatus('rejected')"
>
<CrossIcon /> Reject
</button>
<OverflowMenu
class="btn btn-danger btn-dropdown-animation icon-only"
position="top"
direction="left"
:options="
replyBody
? [
{
id: 'withhold-reply',
color: 'danger',
action: () => {
sendReply('withheld')
},
hoverFilled: true,
disabled: project.status === 'withheld',
},
]
: [
{
id: 'withhold',
color: 'danger',
action: () => {
setStatus('withheld')
},
hoverFilled: true,
disabled: project.status === 'withheld',
},
]
"
>
<DropdownIcon style="rotate: 180deg" />
<template #withhold-reply> <EyeOffIcon /> Withhold with reply </template>
<template #withhold> <EyeOffIcon /> Withhold </template>
</OverflowMenu>
</div>
</template>
</template>
</div>
@@ -168,7 +193,7 @@
</template>
<script setup>
import { MarkdownEditor } from 'omorphia'
import { OverflowMenu, MarkdownEditor, DropdownIcon } from 'omorphia'
import { useImageUpload } from '~/composables/image-upload.ts'
import CopyCode from '~/components/ui/CopyCode.vue'
import ReplyIcon from '~/assets/images/utils/reply.svg'
@@ -199,11 +224,6 @@ const props = defineProps({
required: false,
default: null,
},
updateThread: {
type: Function,
required: false,
default: () => {},
},
setStatus: {
type: Function,
required: false,
@@ -220,6 +240,9 @@ const props = defineProps({
required: true,
},
})
const emit = defineEmits(['update-thread'])
const app = useNuxtApp()
const cosmetics = useCosmetics()
@@ -255,7 +278,7 @@ async function updateThreadLocal() {
if (threadId) {
thread = await useBaseFetch(`thread/${threadId}`)
}
props.updateThread(thread)
emit('update-thread', thread)
}
const imageIDs = ref([])
@@ -270,12 +293,13 @@ async function onUploadImage(file) {
return response.url
}
async function sendReply(status = null) {
async function sendReply(status = null, privateMessage = false) {
try {
const body = {
body: {
type: 'text',
body: replyBody.value,
private: privateMessage,
},
}

View File

@@ -23,9 +23,9 @@
/>
</ConditionalNuxtLink>
<span :class="`message__author role-${members[message.author_id].role}`">
<PrivateIcon
<LockIcon
v-if="message.body.private"
v-tooltip="'Only visible by moderators'"
v-tooltip="'Only visible to moderators'"
class="private-icon"
/>
<ConditionalNuxtLink
@@ -34,15 +34,12 @@
>
{{ members[message.author_id].username }}
</ConditionalNuxtLink>
<ModeratorIcon
v-if="members[message.author_id].role === 'moderator'"
v-tooltip="'Moderator'"
/>
<ScaleIcon v-if="members[message.author_id].role === 'moderator'" v-tooltip="'Moderator'" />
<ModrinthIcon
v-else-if="members[message.author_id].role === 'admin'"
v-tooltip="'Modrinth Team'"
/>
<MicIcon
<MicrophoneIcon
v-if="report && message.author_id === report.reporterUser.id"
v-tooltip="'Reporter'"
class="reporter-icon"
@@ -51,11 +48,11 @@
</template>
<template v-else>
<div class="message__icon backed-svg circle moderation-color" :class="{ raised: raised }">
<ModeratorIcon />
<ScaleIcon />
</div>
<span class="message__author moderation-color">
Moderator
<ModeratorIcon v-tooltip="'Moderator'" />
<ScaleIcon v-tooltip="'Moderator'" />
</span>
</template>
<div
@@ -81,21 +78,40 @@
{{ timeSincePosted }}
</span>
</span>
<!-- <div class="message__actions">-->
<!-- <Button icon-only><MoreIcon /></Button>-->
<!-- </div>-->
<div v-if="isStaff(auth.user) && message.author_id === auth.user.id" class="message__actions">
<OverflowMenu
class="btn btn-transparent icon-only"
:options="[
{
id: 'delete',
action: () => deleteMessage(),
color: 'red',
hoverFilled: true,
},
]"
>
<MoreHorizontalIcon />
<template #delete> <TrashIcon /> Delete </template>
</OverflowMenu>
</div>
</div>
</template>
<script setup>
import {
OverflowMenu,
MoreHorizontalIcon,
TrashIcon,
ConditionalNuxtLink,
MicrophoneIcon,
LockIcon,
ModrinthIcon,
ScaleIcon,
} from 'omorphia'
import Avatar from '~/components/ui/Avatar.vue'
import Badge from '~/components/ui/Badge.vue'
import ModeratorIcon from '~/assets/images/sidebar/admin.svg'
import ModrinthIcon from '~/assets/images/utils/modrinth.svg'
import MicIcon from '~/assets/images/utils/mic.svg'
import PrivateIcon from '~/assets/images/utils/lock.svg'
import { renderString } from '~/helpers/parse.js'
import ConditionalNuxtLink from '~/components/ui/ConditionalNuxtLink.vue'
import { isStaff } from '~/helpers/users.js'
const props = defineProps({
message: {
@@ -122,8 +138,14 @@ const props = defineProps({
type: Boolean,
default: false,
},
auth: {
type: Object,
required: true,
},
})
const emit = defineEmits(['update-thread'])
const formattedMessage = computed(() => {
const body = renderString(props.message.body.body)
if (props.forceCompact) {
@@ -142,6 +164,13 @@ const formattedMessage = computed(() => {
const formatRelativeTime = useRelativeTime()
const timeSincePosted = ref(formatRelativeTime(props.message.created))
async function deleteMessage() {
await useBaseFetch(`message/${props.message.id}`, {
method: 'DELETE',
})
emit('update-thread')
}
</script>
<style lang="scss" scoped>
@@ -222,7 +251,10 @@ const timeSincePosted = ref(formatRelativeTime(props.message.created))
.message__actions {
grid-area: actions;
margin-left: auto;
opacity: 0;
@media (hover: hover) {
opacity: 0;
}
}
.message__body {

View File

@@ -14,6 +14,7 @@
:message="message"
:report="report"
:members="members"
:auth="auth"
force-compact
no-links
/>

View File

@@ -70,11 +70,11 @@
<ConversationThread
v-if="thread"
:thread="thread"
:update-thread="(newThread) => (thread = newThread)"
:project="project"
:set-status="setStatus"
:current-member="currentMember"
:auth="auth"
@update-thread="(newThread) => (thread = newThread)"
/>
</section>
</div>