dashboard-v2/frontend/src/components/DiscordMessages.svelte
2024-09-20 23:54:45 +01:00

510 lines
15 KiB
Svelte

{#if tagSelectorModal}
<div class="modal" transition:fade>
<div class="modal-wrapper">
<Card footer footerRight fill={false}>
<span slot="title">Send Tag</span>
<div slot="body" class="modal-inner">
<Dropdown col2 label="Select a tag..." bind:value={selectedTag}>
{#each Object.keys(tags) as tag}
<option value={tag}>{tag}</option>
{/each}
</Dropdown>
</div>
<div slot="footer" style="gap: 12px">
<Button danger icon="fas fa-times" on:click={() => tagSelectorModal = false}>Close</Button>
<Button icon="fas fa-paper-plane" on:click={sendTag}>Send</Button>
</div>
</Card>
</div>
</div>
<div class="modal-backdrop" transition:fade>
</div>
{/if}
<section class="discord-container">
<div class="channel-header">
<span class="channel-name">#ticket-{ticketId}</span>
</div>
<div class="message-container" bind:this={container}>
{#each messages as message}
<div class="message">
<img class="avatar" src={getAvatarUrl(message.author.id, message.author.avatar)}
on:error={(e) => handleAvatarLoadError(e, message.author.id)} alt="Avatar"/>
<div>
<div>
<span class="username">{message.author.global_name || message.author.username}</span>
<span class="timestamp">
{new Date() - new Date(message.timestamp) < 86400000 ? getRelativeTime(new Date(message.timestamp)) : new Date(message.timestamp).toLocaleString()}
</span>
</div>
<div class="content">
{#if message.content?.length > 0}
<span class="plaintext">{message.content}</span>
{/if}
{#if message.embeds?.length > 0}
<div class="embed-wrapper">
{#each message.embeds.filter(e => 'color' in e) as embed}
<div class="embed">
<div class="colour" style="background-color: #{embed.color.toString(16)}"></div>
<div class="main">
{#if embed.title}
<b>{embed.title}</b>
{/if}
{#if embed.description}
<span>{embed.description}</span>
{/if}
{#if embed.fields && embed.fields.length > 0}
<div class="fields">
{#each embed.fields as field}
<div class="field" class:inline={field.inline}>
<span class="name">{field.name}</span>
<span class="value">{field.value}</span>
</div>
{/each}
</div>
{/if}
{#if embed.image && embed.image.proxy_url}
<img src={embed.image.proxy_url} alt="Embed Image"/>
{/if}
</div>
</div>
{/each}
</div>
{/if}
{#if message.attachments?.length > 0}
<div class="attachment-wrapper">
{#each message.attachments.filter(a => isImage(a.filename)) as attachment}
{@const proxyUrl = attachment.proxy_url.replaceAll("\u0026", "&")}
<img src={proxyUrl} alt="{attachment.filename}"/>
{/each}
{#each message.attachments.filter(a => !isImage(a.filename)) as attachment}
{@const directUrl = attachment.url.replaceAll("\u0026", "&")}
{@const proxyUrl = attachment.proxy_url.replaceAll("\u0026", "&")}
<div class="other">
<div class="metadata">
<span class="name">{attachment.filename}</span>
<span class="size">{formatFileSize(attachment.size)}</span>
</div>
<a href="{isCdnUrl(directUrl) ? directUrl : proxyUrl}" target="_blank"
download="{attachment.filename}">
<i class="fa-solid fa-download"></i>
</a>
</div>
{/each}
</div>
{/if}
</div>
</div>
</div>
{/each}
</div>
<div class="input-container">
<form on:submit|preventDefault={sendMessage}>
<input type="text" class="message-input" bind:value={sendContent} disabled={!isPremium}
placeholder="{isPremium ? `Message #ticket-${ticketId}` : 'Premium users can receive messages in real-time and respond to tickets through the dashboard'}">
{#if isPremium}
<i class="fas fa-paper-plane send-button" on:click={sendMessage}/>
<div class="tag-selector">
<Button type="button" noShadow on:click={openTagSelector}>Select Tag</Button>
</div>
{/if}
</form>
</div>
</section>
<script>
import {createEventDispatcher, onMount} from "svelte";
import {fade} from "svelte/transition";
import Button from "./Button.svelte";
import Card from "./Card.svelte";
import Dropdown from "./form/Dropdown.svelte";
import {getAvatarUrl, getDefaultIcon} from "../js/icons";
import {getRelativeTime} from "../js/util";
export let ticketId;
export let isPremium = false;
export let messages = [];
export let container;
export let tags = [];
const dispatch = createEventDispatcher();
let sendContent = '';
let selectedTag;
$: messages, setTimeout(scrollToBottom, 100);
function sendMessage() {
dispatch('send', {
type: 'message',
content: sendContent
});
sendContent = '';
}
let tagSelectorModal = false;
function openTagSelector() {
tagSelectorModal = true;
window.scrollTo({top: 0, behavior: 'smooth'});
}
function scrollToBottom() {
if (container) {
container.scrollTop = container.scrollHeight;
}
}
function sendTag() {
tagSelectorModal = false;
dispatch('send', {
type: 'tag',
tag_id: selectedTag
});
selectedTag = undefined;
}
let failed = [];
function handleAvatarLoadError(e, userId) {
if (!failed.includes(userId)) {
failed.push(userId);
e.target.src = getDefaultIcon(userId);
}
}
function isImage(fileName) {
const imageExtensions = ['png', 'jpg', 'jpeg', 'gif', 'gifv', 'webp'];
return imageExtensions.includes(fileName.split('.').pop().toLowerCase());
}
function formatFileSize(size) {
if (size < 1024) return `${size} B`;
if (size < 1024 * 1024) return `${(size / 1024).toFixed(0)} KB`;
if (size < 1024 * 1024 * 1024) return `${(size / 1024 / 1024).toFixed(0)} MB`;
else return `${(size / 1024 / 1024 / 1024).toFixed(1)} GB`;
}
function isCdnUrl(url) {
const parsed = new URL(url);
return parsed.hostname === 'cdn.discordapp.com';
}
onMount(() => {
messages = messages.map(message => {
// Sort attachments; image first
message.attachments = message.attachments.sort((a, b) => {
if (isImage(a.filename) && !isImage(b.filename)) {
return -1;
} else if (!isImage(a.filename) && isImage(b.filename)) {
return 1;
} else {
return 0;
}
});
return message;
})
});
</script>
<style>
.discord-container {
display: flex;
flex-direction: column;
background-color: #2e3136;
border-radius: 4px;
height: 80vh;
max-height: 100vh;
margin: 0;
padding: 0;
font-family: 'Poppins', sans-serif !important;
}
.channel-header {
display: flex;
align-items: center;
background-color: #1e2124;
height: 5vh;
width: 100%;
border-radius: 4px 4px 0 0;
position: relative;
text-align: center;
}
.channel-name {
color: white;
font-weight: bold;
padding-left: 20px;
}
.message-container {
display: flex;
flex-direction: column;
flex: 1;
gap: 10px;
position: relative;
overflow-y: scroll;
overflow-wrap: break-word;
padding: 5px 10px;
}
.message {
display: flex;
flex-direction: row;
gap: 10px;
}
.message:first-child {
margin-top: 5px;
}
.avatar {
width: 36px;
height: 36px;
border-radius: 50%;
}
.message > div {
display: flex;
flex-direction: column;
line-height: 16px;
}
.username {
color: white;
font-weight: bold;
font-size: 16px;
}
.timestamp {
font-size: 11px;
opacity: 0.6;
}
.plaintext {
font-size: 14px;
}
.embed-wrapper {
display: flex;
flex-direction: column;
gap: 5px;
margin-top: 5px;
}
.embed {
display: flex;
flex-direction: row;
gap: 5px;
width: 100%;
min-width: 300px;
border-radius: 5px;
background-color: #272727;
}
.embed > .colour {
width: 4px;
border-radius: 5px 0 0 5px;
}
.embed > .main {
display: flex;
flex-direction: column;
padding: 10px 10px 10px 5px;
width: 100%;
white-space: pre-wrap;
}
.embed > .main > span {
font-size: 14px;
}
.fields {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 5px;
}
.fields:not(:first-child) {
margin-top: 10px;
}
.field {
display: flex;
flex-direction: column;
}
.field.inline {
flex: 0 0 calc(33.3333% - 5px);
}
.field:not(.inline) {
flex-basis: 100%;
}
.field > .name {
font-size: 14px;
font-weight: bold;
}
.field > .value {
font-size: 14px;
}
.embed > .main > img {
width: 100%;
max-width: 300px;
margin-top: 5px;
border-radius: 3px;
}
.attachment-wrapper {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 5px;
row-gap: 5px;
margin-top: 5px;
}
.attachment-wrapper > img {
box-sizing: border-box;
width: 40%;
min-width: 300px;
border-radius: 5px;
}
.attachment-wrapper > .other {
display: flex;
flex-direction: row;
align-items: center;
gap: 10px;
padding: 5px 10px;
border-radius: 5px;
background-color: #272727;
}
.attachment-wrapper > .other > .metadata {
display: flex;
flex-direction: column;
gap: 2px;
}
.attachment-wrapper > .other > .metadata > .name {
font-size: 14px;
font-weight: bold;
}
.attachment-wrapper > .other > .metadata > .size {
font-size: 12px;
opacity: 0.6;
}
.attachment-wrapper > .other i {
font-size: 24px;
color: white;
opacity: 0.8;
cursor: pointer;
}
.message-container:last-child {
margin-bottom: 5px;
}
.message-input {
display: flex;
flex: 1;
font-size: 16px;
line-height: 24px;
height: 40px;
padding: 8px;
border-color: #2e3136 !important;
background-color: #2e3136 !important;
color: white !important;
}
.message-input:focus, .message-input:focus-visible {
outline-width: 0;
}
form {
display: flex;
flex-direction: row;
align-items: center;
}
.send-button {
margin-right: 8px;
cursor: pointer;
}
.tag-selector {
margin-right: 4px;
}
/** modal **/
.modal {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 999;
display: flex;
justify-content: center;
align-items: center;
}
.modal-wrapper {
display: flex;
width: 60%;
margin: 10% auto auto auto;
}
.modal-inner {
display: flex;
flex-direction: row;
justify-content: flex-start;
gap: 2%;
width: 100%;
}
@media only screen and (max-width: 1280px) {
.modal-wrapper {
width: 96%;
}
}
.modal-backdrop {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 500;
background-color: #000;
opacity: .5;
}
</style>