add basic model and interface
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
import * as React from "bloatless-react";
|
||||
|
||||
import { MessageComposer } from "../Views/messageComposer";
|
||||
import { ThreadView } from "../Views/threadView";
|
||||
import { translation } from "../translations";
|
||||
|
||||
export function MessageTab() {
|
||||
return (
|
||||
<article id="message-tab">
|
||||
<header>{translation.messages}</header>
|
||||
{ThreadView()}
|
||||
<footer>{MessageComposer()}</footer>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import * as React from "bloatless-react";
|
||||
|
||||
import { CommunicationSection } from "../Views/communicationSection";
|
||||
import { ConnectionSection } from "../Views/connectionSection";
|
||||
import { EncryptionSection } from "../Views/encryptionSection";
|
||||
import { isConnected } from "../model";
|
||||
import { translation } from "../translations";
|
||||
|
||||
export function SettingsTab() {
|
||||
return (
|
||||
<article id="settings-tab" toggle:connected={isConnected}>
|
||||
<header>{translation.settings}</header>
|
||||
<div>
|
||||
{ConnectionSection()}
|
||||
{CommunicationSection()}
|
||||
{EncryptionSection()}
|
||||
<hr class="mobile-only"></hr>
|
||||
<a
|
||||
href="#message-tab"
|
||||
class="mobile-only width-100 flex-row justify-end control-gap"
|
||||
>
|
||||
{translation.messages}
|
||||
<span class="icon">arrow_forward</span>
|
||||
</a>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
import * as React from "bloatless-react";
|
||||
|
||||
import {
|
||||
Channel,
|
||||
addSecondaryChannel,
|
||||
cannotAddSecondaryChannel,
|
||||
cannotSetChannel,
|
||||
newSecondaryChannelName,
|
||||
primaryChannel,
|
||||
removeSecondaryChannel,
|
||||
secondaryChannels,
|
||||
senderName,
|
||||
setChannel,
|
||||
} from "../model";
|
||||
|
||||
import { translation } from "../translations";
|
||||
|
||||
const secondaryChannelConverter: React.ListItemConverter<Channel> = (
|
||||
channel
|
||||
) => {
|
||||
function remove() {
|
||||
removeSecondaryChannel(channel);
|
||||
}
|
||||
|
||||
return (
|
||||
<div class="tile padding-0">
|
||||
<div class="flex-row justify-apart align-center">
|
||||
<b class="padding-h">{channel.channelName}</b>
|
||||
|
||||
<button
|
||||
class="danger"
|
||||
aria-label={translation.removeSecondaryChannel}
|
||||
on:click={remove}
|
||||
>
|
||||
<span class="icon">delete</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export function CommunicationSection() {
|
||||
return (
|
||||
<div>
|
||||
<h2>{translation.communication}</h2>
|
||||
|
||||
<label class="tile">
|
||||
<span class="icon">account_circle</span>
|
||||
<div>
|
||||
<span>{translation.yourName}</span>
|
||||
<input
|
||||
bind:value={senderName}
|
||||
placeholder={translation.yourNamePlaceholder}
|
||||
></input>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<hr></hr>
|
||||
|
||||
<label class="tile">
|
||||
<span class="icon">forum</span>
|
||||
<div>
|
||||
<span>{translation.primaryChannel}</span>
|
||||
<input
|
||||
bind:value={primaryChannel}
|
||||
on:enter={setChannel}
|
||||
placeholder={translation.channelPlaceholder}
|
||||
></input>
|
||||
</div>
|
||||
</label>
|
||||
<div class="flex-row width-input justify-end">
|
||||
<button
|
||||
class="primary width-50"
|
||||
on:click={setChannel}
|
||||
toggle:disabled={cannotSetChannel}
|
||||
>
|
||||
{translation.setInput}
|
||||
<span class="icon">check</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<hr></hr>
|
||||
|
||||
<div class="flex-row width-input margin-bottom">
|
||||
<input
|
||||
bind:value={newSecondaryChannelName}
|
||||
on:enter={addSecondaryChannel}
|
||||
placeholder={translation.newSecondaryChannelPlaceholder}
|
||||
class="width-100 flex"
|
||||
></input>
|
||||
<button
|
||||
class="primary"
|
||||
on:click={addSecondaryChannel}
|
||||
toggle:disabled={cannotAddSecondaryChannel}
|
||||
aria-label={translation.addSecondaryChannel}
|
||||
>
|
||||
<span class="icon">add</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex-column gap"
|
||||
subscribe:children={[secondaryChannels, secondaryChannelConverter]}
|
||||
></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import * as React from "bloatless-react";
|
||||
|
||||
import { ConnectionInputView } from "./connectionSettingsView";
|
||||
import { ConnectionStatusView } from "./connectionStatusView";
|
||||
import { translation } from "../translations";
|
||||
|
||||
export function ConnectionSection() {
|
||||
return (
|
||||
<div>
|
||||
<h2>{translation.connection}</h2>
|
||||
{ConnectionStatusView()}
|
||||
<hr></hr>
|
||||
{ConnectionInputView()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import * as React from "bloatless-react";
|
||||
|
||||
import {
|
||||
cannotConnect,
|
||||
connect,
|
||||
serverAddress,
|
||||
} from "../model";
|
||||
|
||||
import { translation } from "../translations";
|
||||
|
||||
export function ConnectionInputView() {
|
||||
return (
|
||||
<div class="flex-column">
|
||||
<label class="tile">
|
||||
<span class="icon">cell_tower</span>
|
||||
<div>
|
||||
<span>{translation.serverAddress}</span>
|
||||
<input
|
||||
bind:value={serverAddress}
|
||||
on:enter={connect}
|
||||
placeholder={translation.serverAddressPlaceholder}
|
||||
></input>
|
||||
</div>
|
||||
</label>
|
||||
<div class="flex-row width-input justify-end">
|
||||
<button
|
||||
class="primary width-50"
|
||||
on:click={connect}
|
||||
toggle:disabled={cannotConnect}
|
||||
>
|
||||
{translation.connectToServer}
|
||||
<span class="icon">arrow_forward</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import * as React from "bloatless-react";
|
||||
|
||||
import { translation } from "../translations";
|
||||
|
||||
export function ConnectionStatusView() {
|
||||
return (
|
||||
<div class="tile width-input">
|
||||
<div>
|
||||
<b>{translation.connectionStatus}</b>
|
||||
<span
|
||||
class="success connected-only"
|
||||
subscribe:innerText={translation.connectedTo}
|
||||
></span>
|
||||
<span class="error disconnected-only">{translation.disconnected}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import * as React from "bloatless-react";
|
||||
|
||||
import { encryptionKey } from "../model";
|
||||
import { translation } from "../translations";
|
||||
|
||||
const shouldShowKey = new React.State(false);
|
||||
const inputType = React.createProxyState([shouldShowKey], () =>
|
||||
shouldShowKey.value ? "text" : "password"
|
||||
);
|
||||
|
||||
export function EncryptionSection() {
|
||||
return (
|
||||
<div>
|
||||
<h2>{translation.encryption}</h2>
|
||||
<label class="tile">
|
||||
<span class="icon">key</span>
|
||||
<div>
|
||||
<span>{translation.encryptionKey}</span>
|
||||
<input
|
||||
bind:value={encryptionKey}
|
||||
set:type={inputType}
|
||||
placeholder={translation.encryptionKeyPlaceholder}
|
||||
></input>
|
||||
</div>
|
||||
</label>
|
||||
<label class="inline">
|
||||
<input type="checkbox" bind:checked={shouldShowKey}></input>
|
||||
{translation.showEncryptionKey}
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import * as React from "bloatless-react";
|
||||
|
||||
import { cannotSendMessage, messageBody, sendMessage } from "../model";
|
||||
|
||||
import { translation } from "../translations";
|
||||
|
||||
export function MessageComposer() {
|
||||
return (
|
||||
<div class="flex-row width-100">
|
||||
<input
|
||||
class="width-100 flex-1"
|
||||
style="max-width: unset"
|
||||
placeholder={translation.composerPlaceholder}
|
||||
bind:value={messageBody}
|
||||
on:enter={sendMessage}
|
||||
></input>
|
||||
<button
|
||||
class="primary"
|
||||
on:click={sendMessage}
|
||||
toggle:disabled={cannotSendMessage}
|
||||
>
|
||||
<span class="icon">send</span>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import * as React from "bloatless-react";
|
||||
|
||||
import {
|
||||
Message,
|
||||
messages,
|
||||
} from "../model";
|
||||
|
||||
const messageConverter: React.ListItemConverter<Message> = (message) => {
|
||||
return (
|
||||
<div class="tile width-100 flex-no">
|
||||
<div class="flex-column">
|
||||
<span class="secondary">
|
||||
{message.sender}@{message.channel}
|
||||
</span>
|
||||
<b>{message.body}</b>
|
||||
<span class="secondary">
|
||||
{new Date(message.isoDate).toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export function ThreadView() {
|
||||
return (
|
||||
<div class="flex-column gap" subscribe:children={[messages, messageConverter]}></div>
|
||||
);
|
||||
}
|
||||
+6
-15
@@ -1,19 +1,10 @@
|
||||
import "./styles.css";
|
||||
|
||||
import * as React from "bloatless-react";
|
||||
|
||||
import { MessageTab } from "./messageTab";
|
||||
import { SettingsTab } from "./settingsTab";
|
||||
import { getText } from "./translations";
|
||||
import { MessageTab } from "./Tabs/messageTab";
|
||||
import { SettingsTab } from "./Tabs/settingsTab";
|
||||
import { translation } from "./translations";
|
||||
|
||||
document.body.prepend(
|
||||
<menu>
|
||||
<a class="tab-link" href="#settings-tab" active>
|
||||
<span class="icon">settings</span>
|
||||
{getText("settings")}
|
||||
</a>
|
||||
<a class="tab-link" href="#message-tab">
|
||||
<span class="icon">forum</span>
|
||||
{getText("messages")}
|
||||
</a>
|
||||
</menu>
|
||||
);
|
||||
document.querySelector("main")!.append(SettingsTab(), MessageTab());
|
||||
document.querySelector("main")!.classList.add("split");
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
import * as React from "bloatless-react";
|
||||
|
||||
import { getText } from "./translations";
|
||||
|
||||
export function MessageTab() {
|
||||
return (
|
||||
<article id="message-tab">
|
||||
<header>{getText("messages")}</header>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
+60
-25
@@ -30,23 +30,46 @@ export class Message implements React.Identifiable {
|
||||
|
||||
// STATE
|
||||
// connection
|
||||
export const socketAddress = React.restoreState("socket-address", "");
|
||||
export const currentAddress = new React.State("");
|
||||
export const serverAddress = React.restoreState("socket-address", "");
|
||||
export const isConnected = new React.State(false);
|
||||
|
||||
// sending
|
||||
let currentChannel: string | undefined = undefined;
|
||||
export const primaryChannel = React.restoreState("primary-channel", "");
|
||||
export const newAdditionalChannelName = new React.State("");
|
||||
export const additionalChannels = React.restoreListState<Channel>(
|
||||
"additional-channels"
|
||||
export const cannotConnect = React.createProxyState(
|
||||
[serverAddress, isConnected],
|
||||
() =>
|
||||
serverAddress.value == "" ||
|
||||
(currentAddress.value == serverAddress.value && isConnected.value == true)
|
||||
);
|
||||
|
||||
// sending
|
||||
export const currentPrimaryChannel = new React.State("");
|
||||
export const primaryChannel = React.restoreState("primary-channel", "");
|
||||
export const newSecondaryChannelName = new React.State("");
|
||||
export const secondaryChannels =
|
||||
React.restoreListState<Channel>("secondary-channels");
|
||||
export const encryptionKey = React.restoreState("encryption-key", "");
|
||||
export const senderName = React.restoreState("sender-name", "");
|
||||
export const message = React.restoreState("message", "");
|
||||
export const messageBody = React.restoreState("message", "");
|
||||
|
||||
export const cannotSetChannel = React.createProxyState(
|
||||
[primaryChannel, currentPrimaryChannel, isConnected],
|
||||
() =>
|
||||
primaryChannel.value == "" ||
|
||||
primaryChannel.value == currentPrimaryChannel.value ||
|
||||
isConnected.value == false
|
||||
);
|
||||
|
||||
export const cannotAddSecondaryChannel = React.createProxyState(
|
||||
[newSecondaryChannelName],
|
||||
() => newSecondaryChannelName.value == ""
|
||||
);
|
||||
|
||||
export const cannotSendMessage = React.createProxyState(
|
||||
[primaryChannel, message],
|
||||
() => primaryChannel.value == "" || message.value == ""
|
||||
[currentPrimaryChannel, messageBody, senderName],
|
||||
() =>
|
||||
currentPrimaryChannel.value == "" ||
|
||||
messageBody.value == "" ||
|
||||
senderName.value == ""
|
||||
);
|
||||
|
||||
// receiving
|
||||
@@ -54,19 +77,21 @@ export const messages = React.restoreListState<Message>("messages");
|
||||
|
||||
// METHODS
|
||||
export function connect() {
|
||||
UDN.connect(socketAddress.value);
|
||||
if (cannotConnect.value == true) return;
|
||||
currentAddress.value = serverAddress.value;
|
||||
UDN.connect(serverAddress.value);
|
||||
}
|
||||
|
||||
export function send() {
|
||||
export function sendMessage() {
|
||||
if (cannotSendMessage.value == true) return;
|
||||
|
||||
// get channels
|
||||
const additionalChannelNames: string[] = [
|
||||
...additionalChannels.value.values(),
|
||||
const secondaryChannelNames: string[] = [
|
||||
...secondaryChannels.value.values(),
|
||||
].map((channel) => channel.channelName);
|
||||
const allChannelNames: string[] = [
|
||||
primaryChannel.value,
|
||||
...additionalChannelNames,
|
||||
...secondaryChannelNames,
|
||||
];
|
||||
|
||||
// create object
|
||||
@@ -74,28 +99,37 @@ export function send() {
|
||||
const messageObject = new Message(
|
||||
joinedChannelName,
|
||||
senderName.value,
|
||||
message.value,
|
||||
messageBody.value,
|
||||
new Date().toISOString()
|
||||
);
|
||||
const messageString = JSON.stringify(messageObject);
|
||||
|
||||
// send
|
||||
UDN.sendMessage(joinedChannelName, messageString);
|
||||
|
||||
// clear
|
||||
messageBody.value = "";
|
||||
}
|
||||
|
||||
export function setChannel(): void {
|
||||
if (currentChannel != undefined) {
|
||||
UDN.unsubscribe(currentChannel);
|
||||
if (cannotSetChannel.value == true) return;
|
||||
|
||||
if (currentPrimaryChannel != undefined) {
|
||||
UDN.unsubscribe(currentPrimaryChannel.value);
|
||||
}
|
||||
UDN.subscribe(primaryChannel.value);
|
||||
}
|
||||
|
||||
export function addAdditionalChannel(): void {
|
||||
const channelObject = new Channel(newAdditionalChannelName.value);
|
||||
additionalChannels.add(channelObject);
|
||||
export function addSecondaryChannel(): void {
|
||||
if (cannotAddSecondaryChannel.value == true) return;
|
||||
|
||||
const channelObject = new Channel(newSecondaryChannelName.value);
|
||||
secondaryChannels.add(channelObject);
|
||||
newSecondaryChannelName.value = "";
|
||||
}
|
||||
|
||||
export function removeAdditionalChannel(channel: Channel): void {
|
||||
additionalChannels.remove(channel);
|
||||
export function removeSecondaryChannel(channel: Channel): void {
|
||||
secondaryChannels.remove(channel);
|
||||
}
|
||||
|
||||
// LISTENERS
|
||||
@@ -104,7 +138,7 @@ UDN.onconnect = () => {
|
||||
};
|
||||
|
||||
UDN.onmessage = (data) => {
|
||||
if (data.subscribed && data.messageChannel) {
|
||||
if (data.subscribed != undefined && data.messageChannel != undefined) {
|
||||
return handleSubscriptionConfirmation(data.messageChannel, data.subscribed);
|
||||
} else if (data.messageBody) {
|
||||
handleMessage(data.messageBody);
|
||||
@@ -120,8 +154,9 @@ function handleSubscriptionConfirmation(
|
||||
channel: string,
|
||||
isSubscribed: boolean
|
||||
): void {
|
||||
console.log(channel, isSubscribed);
|
||||
if (isSubscribed == true) {
|
||||
currentChannel = channel;
|
||||
currentPrimaryChannel.value = channel;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
import * as React from "bloatless-react";
|
||||
|
||||
import { getText } from "./translations";
|
||||
|
||||
export function SettingsTab() {
|
||||
return (
|
||||
<article id="settings-tab">
|
||||
<header>{getText("settings")}</header>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
#settings-tab > div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
#settings-tab > div > div {
|
||||
width: var(--max-input-width);
|
||||
min-width: 100%;
|
||||
max-width: 100%;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* CONNECTION */
|
||||
|
||||
article:not([connected]) .connected-only {
|
||||
display: none;
|
||||
}
|
||||
|
||||
article:not([connected]) .disconnected-only {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
article[connected] .connected-only {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
article[connected] .disconnected-only {
|
||||
display: none;
|
||||
}
|
||||
+36
-9
@@ -1,19 +1,46 @@
|
||||
import * as React from "bloatless-react";
|
||||
|
||||
import { currentAddress, primaryChannel } from "./model";
|
||||
|
||||
const englishTranslations = {
|
||||
// general
|
||||
setInput: "Set",
|
||||
connectionStatus: "Connection status",
|
||||
connectedTo: React.createProxyState(
|
||||
[currentAddress],
|
||||
() => `Connected to ${currentAddress}`
|
||||
),
|
||||
disconnected: "Disconnected",
|
||||
|
||||
// settings
|
||||
settings: "Settings",
|
||||
connection: "Connection",
|
||||
communication: "Communication",
|
||||
encryption: "Encryption",
|
||||
serverAddress: "Server Address",
|
||||
serverAddressPlaceholder: "ws://192.168.0.69:3000",
|
||||
connectToServer: "Connect",
|
||||
primaryChannel: "Primary channel",
|
||||
channelPlaceholder: "my-channel",
|
||||
addSecondaryChannel: "Add secondary channel",
|
||||
removeSecondaryChannel: "Remove secondary channel",
|
||||
newSecondaryChannelPlaceholder: "Add secondary channel",
|
||||
encryptionKey: "Encryption key",
|
||||
encryptionKeyPlaceholder: "1jg028ej40d",
|
||||
showEncryptionKey: "Show encryption key",
|
||||
yourName: "Your Name",
|
||||
yourNamePlaceholder: "Jane Doe",
|
||||
|
||||
// messages
|
||||
messages: "Messages"
|
||||
messages: "Messages",
|
||||
composerPlaceholder: "Type a message...",
|
||||
sendMessage: "Send"
|
||||
};
|
||||
|
||||
const translations = {
|
||||
const allTranslations = {
|
||||
en: englishTranslations,
|
||||
};
|
||||
|
||||
export function getText(key: keyof typeof englishTranslations): string {
|
||||
const language = navigator.language.substring(0, 2);
|
||||
if (translations[language]) {
|
||||
return translations[language][key];
|
||||
}
|
||||
return translations.en[key];
|
||||
}
|
||||
const language = navigator.language.substring(0, 2);
|
||||
export const translation: typeof englishTranslations =
|
||||
allTranslations[language] ?? allTranslations.en;
|
||||
|
||||
Reference in New Issue
Block a user