- Add textarea for input
- Remove channel switch button - Integrate channel switch selection into channel name - Fix state() warnings
This commit is contained in:
@@ -0,0 +1,79 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {isChannelLocked, channelOptions} from "$lib/shared.svelte";
|
||||||
|
|
||||||
|
let selectElement: HTMLSelectElement;
|
||||||
|
|
||||||
|
async function requestChannelSwitch(event: Event) {
|
||||||
|
if (!event.currentTarget)
|
||||||
|
return;
|
||||||
|
|
||||||
|
let element = (event.currentTarget as HTMLSelectElement);
|
||||||
|
let requestedChannel = element.value;
|
||||||
|
|
||||||
|
console.log(element.value)
|
||||||
|
element.value = '0';
|
||||||
|
|
||||||
|
const rawResponse = await fetch('/channel', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ channel: requestedChannel })
|
||||||
|
});
|
||||||
|
// const content = await rawResponse.json();
|
||||||
|
// TODO: use the response
|
||||||
|
}
|
||||||
|
|
||||||
|
let canvas: HTMLCanvasElement | null = null;
|
||||||
|
function getTextWidth(text: string): number {
|
||||||
|
// re-use canvas object for better performance
|
||||||
|
if (canvas === null)
|
||||||
|
canvas = document.createElement("canvas");
|
||||||
|
|
||||||
|
const context: CanvasRenderingContext2D | null = canvas.getContext("2d");
|
||||||
|
if (!context)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
context.font = getCanvasFont(selectElement);
|
||||||
|
const metrics = context.measureText(text);
|
||||||
|
return metrics.width;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCssStyle(element: Element, prop: string): string {
|
||||||
|
return window.getComputedStyle(element, null).getPropertyValue(prop);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCanvasFont(el = document.body) {
|
||||||
|
const fontWeight = getCssStyle(el, 'font-weight') || 'normal';
|
||||||
|
const fontSize = getCssStyle(el, 'font-size') || '16px';
|
||||||
|
const fontFamily = getCssStyle(el, 'font-family') || 'Times New Roman';
|
||||||
|
|
||||||
|
return `${fontWeight} ${fontSize} ${fontFamily}`;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<select
|
||||||
|
bind:this={selectElement}
|
||||||
|
id="channel-select"
|
||||||
|
style="pointer-events: {isChannelLocked.locked ? 'none' : 'inherit'}; width: {(channelOptions.length > 1 ? getTextWidth(channelOptions[0].text) : 1) + 40}px"
|
||||||
|
onchange={(e) => requestChannelSwitch(e)}>
|
||||||
|
{#each channelOptions as channelOption}
|
||||||
|
{#if channelOption.preview }
|
||||||
|
<option selected disabled hidden value={channelOption.value}>
|
||||||
|
{channelOption.text}
|
||||||
|
</option>
|
||||||
|
{:else}
|
||||||
|
<option value={channelOption.value}>
|
||||||
|
{channelOption.text}
|
||||||
|
</option>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
select {
|
||||||
|
border: none;
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
let textarea: HTMLTextAreaElement;
|
||||||
|
let content: string = $state('');
|
||||||
|
|
||||||
|
function preventNewlines(e: KeyboardEvent) {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
// Prevent key from creating a newline
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
// submit the data
|
||||||
|
const newEvent = new Event('submit', {bubbles: true, cancelable: true});
|
||||||
|
if (e.currentTarget !== null) {
|
||||||
|
(e.currentTarget as HTMLTextAreaElement).closest('form')?.dispatchEvent(newEvent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function resize() {
|
||||||
|
if (!textarea)
|
||||||
|
return;
|
||||||
|
|
||||||
|
textarea.style.height = '1px';
|
||||||
|
textarea.style.height = `${textarea.scrollHeight}px`;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<textarea
|
||||||
|
bind:this={textarea}
|
||||||
|
bind:value={content}
|
||||||
|
oninput={() => resize()}
|
||||||
|
onkeydown={(e) => preventNewlines(e)}
|
||||||
|
|
||||||
|
id="chat-input"
|
||||||
|
autocomplete="off"
|
||||||
|
placeholder="Message"
|
||||||
|
enterkeyhint="send"
|
||||||
|
maxlength="500">
|
||||||
|
</textarea>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
textarea {
|
||||||
|
flex-grow: 0;
|
||||||
|
|
||||||
|
font-size: 1rem;
|
||||||
|
border: 3px solid transparent;
|
||||||
|
border-radius: 20px;
|
||||||
|
background-color: var(--bg-input);
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: 2px solid var(--focus-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
padding: 5px 20px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
+13
-46
@@ -1,9 +1,7 @@
|
|||||||
import {type Source, source} from "sveltekit-sse";
|
import {type Source, source} from "sveltekit-sse";
|
||||||
|
import {channelOptions, isChannelLocked} from "$lib/shared.svelte";
|
||||||
|
|
||||||
interface ChatElements {
|
interface ChatElements {
|
||||||
channelHint: HTMLElement | null,
|
|
||||||
channelSelect: HTMLElement | null,
|
|
||||||
|
|
||||||
messagesContainer: Element | null,
|
messagesContainer: Element | null,
|
||||||
messagesList: HTMLElement | null,
|
messagesList: HTMLElement | null,
|
||||||
|
|
||||||
@@ -35,6 +33,7 @@ interface Template {
|
|||||||
// ref `DataStructure.SwitchChannel`
|
// ref `DataStructure.SwitchChannel`
|
||||||
interface SwitchChannel {
|
interface SwitchChannel {
|
||||||
channelName: Template[];
|
channelName: Template[];
|
||||||
|
channelValue: number;
|
||||||
channelLocked: boolean;
|
channelLocked: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,7 +57,6 @@ export class ChatTwoWeb {
|
|||||||
elements!: ChatElements;
|
elements!: ChatElements;
|
||||||
maxTimestampWidth: number = 0;
|
maxTimestampWidth: number = 0;
|
||||||
scrolledToBottom: boolean = true;
|
scrolledToBottom: boolean = true;
|
||||||
channelLocked: boolean = false;
|
|
||||||
|
|
||||||
sse!: EventSource;
|
sse!: EventSource;
|
||||||
connection!: Source;
|
connection!: Source;
|
||||||
@@ -70,8 +68,8 @@ export class ChatTwoWeb {
|
|||||||
|
|
||||||
setupDOMElements() {
|
setupDOMElements() {
|
||||||
this.elements = {
|
this.elements = {
|
||||||
channelHint: document.getElementById('channel-hint'),
|
// channelHint: document.getElementById('channel-hint'),
|
||||||
channelSelect: document.getElementById('channel-select'),
|
// channelSelect: document.getElementById('channel-select'),
|
||||||
|
|
||||||
messagesContainer: document.querySelector('#messages > .scroll-container')!,
|
messagesContainer: document.querySelector('#messages > .scroll-container')!,
|
||||||
messagesList: document.getElementById('messages-list'),
|
messagesList: document.getElementById('messages-list'),
|
||||||
@@ -82,23 +80,6 @@ export class ChatTwoWeb {
|
|||||||
chatInput: document.getElementById('chat-input')
|
chatInput: document.getElementById('chat-input')
|
||||||
};
|
};
|
||||||
|
|
||||||
// channel selector
|
|
||||||
this.elements.channelSelect?.addEventListener('change', async (event) => {
|
|
||||||
if (event.currentTarget === null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
const rawResponse = await fetch('/channel', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Accept': 'application/json',
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ channel: (event.currentTarget as HTMLSelectElement).value })
|
|
||||||
});
|
|
||||||
// const content = await rawResponse.json();
|
|
||||||
// TODO: use the response
|
|
||||||
});
|
|
||||||
|
|
||||||
// add indicator signaling more messages below
|
// add indicator signaling more messages below
|
||||||
this.elements.messagesContainer?.addEventListener('scroll', (event) => {
|
this.elements.messagesContainer?.addEventListener('scroll', (event) => {
|
||||||
if (event.currentTarget === null)
|
if (event.currentTarget === null)
|
||||||
@@ -167,39 +148,25 @@ export class ChatTwoWeb {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateChannelHint(channel: SwitchChannel) {
|
updateChannelHint(channel: SwitchChannel) {
|
||||||
if (this.elements.channelHint === null || this.elements.channelSelect === null)
|
// Set storage to the current lock state
|
||||||
return;
|
isChannelLocked.locked = channel.channelLocked;
|
||||||
|
|
||||||
this.elements.channelHint.innerHTML = '';
|
|
||||||
|
|
||||||
const channelElement = this.processTemplate(channel.channelName);
|
const channelElement = this.processTemplate(channel.channelName);
|
||||||
|
if (!channelElement.firstChild)
|
||||||
// Makes the channel selector unclickable if the channel is fixed
|
|
||||||
this.channelLocked = channel.channelLocked;
|
|
||||||
if (this.channelLocked) {
|
|
||||||
if (channelElement.firstChild === null)
|
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// @ts-ignore
|
let channelName = (channelElement.firstChild as HTMLSpanElement).innerText;
|
||||||
channelElement.firstChild.innerText = `(Locked) ${channelElement.firstChild.innerText}`;
|
if (channel.channelLocked)
|
||||||
this.elements.channelSelect.style.pointerEvents = 'none';
|
channelName = `(Locked) ${channelName}`;
|
||||||
} else {
|
|
||||||
this.elements.channelSelect.style.removeProperty('pointer-events');
|
|
||||||
}
|
|
||||||
|
|
||||||
this.elements.channelHint.appendChild(channelElement);
|
channelOptions[0] = {text: channelName, value: 0, preview: true }
|
||||||
}
|
}
|
||||||
|
|
||||||
updateChannels(channelList: ChannelList) {
|
updateChannels(channelList: ChannelList) {
|
||||||
if (this.elements.channelSelect === null)
|
channelOptions.length = 1;
|
||||||
return;
|
|
||||||
|
|
||||||
this.elements.channelSelect.innerHTML = '';
|
|
||||||
for (const [ label, channel ] of Object.entries(channelList.channels)) {
|
for (const [ label, channel ] of Object.entries(channelList.channels)) {
|
||||||
const option = document.createElement('option');
|
channelOptions.push( { text: label, value: channel, preview: false } )
|
||||||
option.value = channel.toString();
|
|
||||||
option.innerText = label;
|
|
||||||
this.elements.channelSelect.appendChild(option);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
export const isChannelLocked: { locked: boolean } = $state({ locked: false });
|
||||||
|
export const channelOptions: ChannelOption[] = $state([ { text: 'Invalid', value: 0, preview: true } ]);
|
||||||
|
|
||||||
|
export interface ChannelOption {
|
||||||
|
text: string;
|
||||||
|
value: number;
|
||||||
|
preview: boolean;
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
import { page } from '$app/state'
|
import { page } from '$app/state'
|
||||||
import { Alert } from '@sveltestrap/sveltestrap';
|
import { Alert } from '@sveltestrap/sveltestrap';
|
||||||
|
|
||||||
let data: App.Warning | null = null;
|
let data: App.Warning = $state({ hasWarning: false, content: '' });
|
||||||
$effect.pre(() => {
|
$effect.pre(() => {
|
||||||
if (page.url.searchParams.has('message')) {
|
if (page.url.searchParams.has('message')) {
|
||||||
data = {
|
data = {
|
||||||
|
|||||||
@@ -2,10 +2,12 @@
|
|||||||
import { page } from '$app/state'
|
import { page } from '$app/state'
|
||||||
import {Alert} from "@sveltestrap/sveltestrap";
|
import {Alert} from "@sveltestrap/sveltestrap";
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { ChatTwoWeb } from '$lib/chat'
|
import {ChatTwoWeb} from '$lib/chat.svelte'
|
||||||
import {addGfdStylesheet} from "$lib/gfd";
|
import {addGfdStylesheet} from "$lib/gfd";
|
||||||
|
import DynamicTextArea from "../../components/DynamicTextArea.svelte";
|
||||||
|
import ChannelSelector from "../../components/ChannelSelector.svelte";
|
||||||
|
|
||||||
let data: App.Warning | null = null;
|
let data: App.Warning = $state({ hasWarning: false, content: '' });
|
||||||
$effect.pre(() => {
|
$effect.pre(() => {
|
||||||
if (page.url.searchParams.has('message')) {
|
if (page.url.searchParams.has('message')) {
|
||||||
data = {
|
data = {
|
||||||
@@ -51,13 +53,9 @@
|
|||||||
|
|
||||||
<section id="input">
|
<section id="input">
|
||||||
<form>
|
<form>
|
||||||
<div class="select-container">
|
|
||||||
<select id="channel-select"></select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="input-container">
|
<div class="input-container">
|
||||||
<input type="text" id="chat-input" autocomplete="off" placeholder="Message" enterkeyhint="send" maxlength="500">
|
<DynamicTextArea/>
|
||||||
<div id="channel-hint"></div>
|
<ChannelSelector/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button type="submit">Send</button>
|
<button type="submit">Send</button>
|
||||||
|
|||||||
@@ -183,7 +183,7 @@ section#input {
|
|||||||
gap: 10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
input, button, select {
|
input, button {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
border: 3px solid transparent;
|
border: 3px solid transparent;
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
@@ -194,7 +194,7 @@ section#input {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
button, select {
|
button {
|
||||||
padding: 5px 15px;
|
padding: 5px 15px;
|
||||||
border: 3px solid var(--bg-input);
|
border: 3px solid var(--bg-input);
|
||||||
background-image: var(--gradient-clickable);
|
background-image: var(--gradient-clickable);
|
||||||
@@ -207,29 +207,12 @@ section#input {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.select-container, button {
|
button {
|
||||||
position: relative;
|
position: relative;
|
||||||
flex-grow: 0;
|
flex-grow: 0;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.select-container {
|
|
||||||
flex-basis: 0;
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
/* "message-circle" icon from https://github.com/feathericons/feather, under MIT license */
|
|
||||||
mask-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"/></svg>');
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
|
||||||
width: 100%;
|
|
||||||
padding-left: 1.5rem;
|
|
||||||
padding-right: 1.5rem;
|
|
||||||
appearance: none;
|
|
||||||
color: transparent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
button {
|
||||||
padding-left: calc(20px + 1.5rem);
|
padding-left: calc(20px + 1.5rem);
|
||||||
|
|
||||||
@@ -239,7 +222,7 @@ section#input {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.select-container::before, button::before {
|
button::before {
|
||||||
content: '';
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 15px;
|
left: 15px;
|
||||||
@@ -252,11 +235,6 @@ section#input {
|
|||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.select-container::before {
|
|
||||||
left: 50%;
|
|
||||||
transform: translateX(-50%) translateY(-50%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-container {
|
.input-container {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -266,9 +244,9 @@ section#input {
|
|||||||
padding: 5px 20px;
|
padding: 5px 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#channel-hint {
|
#channel-select {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: -1.2em;
|
top: -1.5em;
|
||||||
left: 23px;
|
left: 23px;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -276,7 +254,23 @@ section#input {
|
|||||||
font-weight: 550;
|
font-weight: 550;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
pointer-events: none;
|
|
||||||
|
border: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
background-color: transparent;
|
||||||
|
|
||||||
|
padding: 5px 15px;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: var(--bg-input-hover);
|
||||||
|
background-color: var(--bg-input-hover);
|
||||||
|
background-image: var(--gradient-clickable-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: 2px solid var(--focus-color);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -360,7 +354,7 @@ section#input {
|
|||||||
transform: translateX(-50%) translateY(-50%);
|
transform: translateX(-50%) translateY(-50%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-container #channel-hint {
|
.input-container #channel-select {
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,9 +24,9 @@ public struct ChatTabList(ChatTab[] tabs)
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Contains the current channel name
|
/// Contains the current channel name
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public struct SwitchChannel((MessageTemplate[] ChannelName, bool Locked) channel)
|
public struct SwitchChannel((MessageTemplate[] Name, bool Locked) channel)
|
||||||
{
|
{
|
||||||
[JsonProperty("channelName")] public MessageTemplate[] ChannelName = channel.ChannelName;
|
[JsonProperty("channelName")] public MessageTemplate[] ChannelName = channel.Name;
|
||||||
[JsonProperty("channelLocked")] public bool Locked = channel.Locked;
|
[JsonProperty("channelLocked")] public bool Locked = channel.Locked;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ public class Processing
|
|||||||
Plugin = plugin;
|
Plugin = plugin;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal (MessageTemplate[] ChannelName, bool Locked) ReadChannelName(Chunk[] channelName)
|
internal (MessageTemplate[] Name, bool Locked) ReadChannelName(Chunk[] channelName)
|
||||||
{
|
{
|
||||||
var locked = Plugin.CurrentTab is not { Channel: null };
|
var locked = Plugin.CurrentTab is not { Channel: null };
|
||||||
return (channelName.Select(ProcessChunk).ToArray(), locked);
|
return (channelName.Select(ProcessChunk).ToArray(), locked);
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using ChatTwo.Http.MessageProtocol;
|
using ChatTwo.Code;
|
||||||
|
using ChatTwo.Http.MessageProtocol;
|
||||||
|
|
||||||
namespace ChatTwo.Http;
|
namespace ChatTwo.Http;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user