Render attachments and embeds
This commit is contained in:
parent
89a5ea3b15
commit
3a7e140314
@ -2,18 +2,18 @@ package api
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"github.com/TicketsBot/GoPanel/botcontext"
|
"github.com/TicketsBot/GoPanel/botcontext"
|
||||||
"github.com/TicketsBot/GoPanel/database"
|
dbclient "github.com/TicketsBot/GoPanel/database"
|
||||||
"github.com/TicketsBot/GoPanel/rpc/cache"
|
|
||||||
"github.com/TicketsBot/GoPanel/utils"
|
"github.com/TicketsBot/GoPanel/utils"
|
||||||
|
"github.com/TicketsBot/database"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
cache2 "github.com/rxdn/gdl/cache"
|
"github.com/rxdn/gdl/objects/channel"
|
||||||
|
"github.com/rxdn/gdl/objects/channel/embed"
|
||||||
|
"github.com/rxdn/gdl/objects/user"
|
||||||
"github.com/rxdn/gdl/rest"
|
"github.com/rxdn/gdl/rest"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
var MentionRegex, _ = regexp.Compile("<@(\\d+)>")
|
var MentionRegex, _ = regexp.Compile("<@(\\d+)>")
|
||||||
@ -41,7 +41,7 @@ func GetTicket(ctx *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get the ticket struct
|
// Get the ticket struct
|
||||||
ticket, err := database.Client.Tickets.Get(ctx, ticketId, guildId)
|
ticket, err := dbclient.Client.Tickets.Get(ctx, ticketId, guildId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.JSON(500, gin.H{
|
ctx.JSON(500, gin.H{
|
||||||
"success": true,
|
"success": true,
|
||||||
@ -77,50 +77,56 @@ func GetTicket(ctx *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
messagesFormatted := make([]map[string]interface{}, 0)
|
if ticket.ChannelId == nil {
|
||||||
if ticket.ChannelId != nil {
|
ctx.JSON(404, gin.H{
|
||||||
// Get messages
|
"success": false,
|
||||||
messages, err := rest.GetChannelMessages(context.Background(), botContext.Token, botContext.RateLimiter, *ticket.ChannelId, rest.GetChannelMessagesData{Limit: 100})
|
"error": "Channel ID is nil",
|
||||||
if err != nil {
|
|
||||||
ctx.JSON(500, utils.ErrorJson(err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Format messages, exclude unneeded data
|
|
||||||
for _, message := range utils.Reverse(messages) {
|
|
||||||
content := message.Content
|
|
||||||
|
|
||||||
// Format mentions properly
|
|
||||||
match := MentionRegex.FindAllStringSubmatch(content, -1)
|
|
||||||
for _, mention := range match {
|
|
||||||
if len(mention) >= 2 {
|
|
||||||
mentionedId, err := strconv.ParseUint(mention[1], 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
user, err := cache.Instance.GetUser(context.Background(), mentionedId)
|
|
||||||
if err == nil {
|
|
||||||
content = strings.ReplaceAll(content, fmt.Sprintf("<@%d>", mentionedId), fmt.Sprintf("@%s", user.Username))
|
|
||||||
} else if errors.Is(err, cache2.ErrNotFound) {
|
|
||||||
content = strings.ReplaceAll(content, fmt.Sprintf("<@%d>", mentionedId), "@Unknown User")
|
|
||||||
} else {
|
|
||||||
ctx.JSON(500, utils.ErrorJson(err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
messagesFormatted = append(messagesFormatted, map[string]interface{}{
|
|
||||||
"author": message.Author,
|
|
||||||
"content": content,
|
|
||||||
})
|
})
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
messages, err := fetchMessages(botContext, ticket)
|
||||||
|
if err != nil {
|
||||||
|
ctx.JSON(500, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.JSON(200, gin.H{
|
ctx.JSON(200, gin.H{
|
||||||
"success": true,
|
"success": true,
|
||||||
"ticket": ticket,
|
"ticket": ticket,
|
||||||
"messages": messagesFormatted,
|
"messages": messages,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type StrippedMessage struct {
|
||||||
|
Author user.User `json:"author"`
|
||||||
|
Content string `json:"content"`
|
||||||
|
Timestamp time.Time `json:"timestamp"`
|
||||||
|
Attachments []channel.Attachment `json:"attachments"`
|
||||||
|
Embeds []embed.Embed `json:"embeds"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchMessages(botContext *botcontext.BotContext, ticket database.Ticket) ([]StrippedMessage, error) {
|
||||||
|
// Get messages
|
||||||
|
messages, err := rest.GetChannelMessages(context.Background(), botContext.Token, botContext.RateLimiter, *ticket.ChannelId, rest.GetChannelMessagesData{Limit: 100})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format messages, exclude unneeded data
|
||||||
|
stripped := make([]StrippedMessage, len(messages))
|
||||||
|
for i, message := range utils.Reverse(messages) {
|
||||||
|
stripped[i] = StrippedMessage{
|
||||||
|
Author: message.Author,
|
||||||
|
Content: message.Content,
|
||||||
|
Timestamp: message.Timestamp,
|
||||||
|
Attachments: message.Attachments,
|
||||||
|
Embeds: message.Embeds,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return stripped, nil
|
||||||
|
}
|
||||||
|
@ -24,14 +24,86 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="discord-container">
|
<section class="discord-container">
|
||||||
<div class="channel-header">
|
<div class="channel-header">
|
||||||
<span id="channel-name">#ticket-{ticketId}</span>
|
<span class="channel-name">#ticket-{ticketId}</span>
|
||||||
</div>
|
</div>
|
||||||
<div id="message-container" bind:this={container}>
|
<div class="message-container" bind:this={container}>
|
||||||
{#each messages as message}
|
{#each messages as message}
|
||||||
<div class="message">
|
<div class="message">
|
||||||
<b>{message.author.username}:</b> {message.content}
|
<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 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>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
@ -47,14 +119,16 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import {createEventDispatcher} from "svelte";
|
import {createEventDispatcher, onMount} from "svelte";
|
||||||
import {fade} from "svelte/transition";
|
import {fade} from "svelte/transition";
|
||||||
import Button from "./Button.svelte";
|
import Button from "./Button.svelte";
|
||||||
import Card from "./Card.svelte";
|
import Card from "./Card.svelte";
|
||||||
import Dropdown from "./form/Dropdown.svelte";
|
import Dropdown from "./form/Dropdown.svelte";
|
||||||
|
import {getAvatarUrl, getDefaultIcon} from "../js/icons";
|
||||||
|
import {getRelativeTime} from "../js/util";
|
||||||
|
|
||||||
export let ticketId;
|
export let ticketId;
|
||||||
export let isPremium = false;
|
export let isPremium = false;
|
||||||
@ -92,6 +166,49 @@
|
|||||||
|
|
||||||
selectedTag = undefined;
|
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>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@ -107,7 +224,8 @@
|
|||||||
max-height: 100vh;
|
max-height: 100vh;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
font-family: 'Catamaran', sans-serif !important;
|
/*font-family: 'Catamaran', sans-serif !important;*/
|
||||||
|
font-family: 'Poppins', sans-serif !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.channel-header {
|
.channel-header {
|
||||||
@ -123,28 +241,178 @@
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
#channel-name {
|
.channel-name {
|
||||||
color: white;
|
color: white;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
padding-left: 20px;
|
padding-left: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#message-container {
|
.message-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
gap: 10px;
|
||||||
|
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
|
|
||||||
|
padding-left: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message {
|
.message {
|
||||||
color: white !important;
|
display: flex;
|
||||||
padding-left: 20px;
|
flex-direction: row;
|
||||||
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#message-container:last-child {
|
.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%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fields {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 5px;
|
||||||
|
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;
|
margin-bottom: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,15 +6,27 @@ export function isAnimated(icon) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getIconUrl(id, icon) {
|
export function getIconUrl(id, icon, size = 256) {
|
||||||
if (!icon || icon === "") {
|
if (!icon || icon === "") {
|
||||||
return getDefaultIcon(id);
|
return getDefaultIcon(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isAnimated(icon)) {
|
if (isAnimated(icon)) {
|
||||||
return `https:\/\/cdn.discordapp.com/icons/${id}/${icon}.gif?size=256`
|
return `https:\/\/cdn.discordapp.com/icons/${id}/${icon}.gif?size=${size}`;
|
||||||
} else {
|
} else {
|
||||||
return `https:\/\/cdn.discordapp.com/icons/${id}/${icon}.webp?size=256`
|
return `https:\/\/cdn.discordapp.com/icons/${id}/${icon}.webp?size=${size}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAvatarUrl(id, avatar, size = 256) {
|
||||||
|
if (!avatar || avatar === "") {
|
||||||
|
return getDefaultIcon(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isAnimated(avatar)) {
|
||||||
|
return `https:\/\/cdn.discordapp.com/avatars/${id}/${avatar}.gif?size=${size}`;
|
||||||
|
} else {
|
||||||
|
return `https:\/\/cdn.discordapp.com/avatars/${id}/${avatar}.webp?size=${size}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,22 +6,17 @@
|
|||||||
<div class="section">
|
<div class="section">
|
||||||
<h2 class="section-title">Close Ticket</h2>
|
<h2 class="section-title">Close Ticket</h2>
|
||||||
|
|
||||||
<form on:submit|preventDefault={closeTicket}>
|
<div class="row" style="gap: 20px">
|
||||||
<div class="row" style="max-height: 63px; align-items: flex-end"> <!-- hacky -->
|
<Input label="Close Reason" col2 placeholder="No reason specified" bind:value={closeReason}/>
|
||||||
<div class="col-2" style="margin-bottom: 0 !important;">
|
<div style="display: flex; align-items: flex-end; padding-bottom: 8px">
|
||||||
<Input label="Close Reason" placeholder="No reason specified" col1={true} bind:value={closeReason}/>
|
<Button danger={true} noShadow icon="fas fa-lock" col3 on:click={closeTicket}>Close Ticket
|
||||||
</div>
|
</Button>
|
||||||
<div class="col-3">
|
|
||||||
<div style="margin-left: 30px; margin-bottom: 0.5em">
|
|
||||||
<Button danger={true} noShadow icon="fas fa-lock">Close Ticket</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<h2 class="section-title">View Ticket</h2>
|
<h2 class="section-title">View Ticket</h2>
|
||||||
<DiscordMessages {ticketId} {isPremium} {tags} {messages} bind:container on:send={sendMessage} />
|
<DiscordMessages {ticketId} {isPremium} {tags} {messages} bind:container on:send={sendMessage}/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
@ -34,7 +29,7 @@
|
|||||||
import Button from "../components/Button.svelte";
|
import Button from "../components/Button.svelte";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import {API_URL} from "../js/constants";
|
import {API_URL} from "../js/constants";
|
||||||
import {setDefaultHeaders, getToken} from '../includes/Auth.svelte'
|
import {getToken, setDefaultHeaders} from '../includes/Auth.svelte'
|
||||||
import Input from "../components/form/Input.svelte";
|
import Input from "../components/form/Input.svelte";
|
||||||
import {navigateTo} from "svelte-router-spa";
|
import {navigateTo} from "svelte-router-spa";
|
||||||
import DiscordMessages from "../components/DiscordMessages.svelte";
|
import DiscordMessages from "../components/DiscordMessages.svelte";
|
||||||
|
Loading…
x
Reference in New Issue
Block a user