Vendored
+63
@@ -2,3 +2,66 @@
|
||||
[unread] {
|
||||
border-left: 10px solid var(--text-warn);
|
||||
}
|
||||
:root {
|
||||
--thread-view-height-with-tools: 8rem;
|
||||
--thread-view-width-with-tools: 20rem;
|
||||
--footer-top-line: calc(var(--control-height) + var(--safe-area-bottom));
|
||||
}
|
||||
#message-tab {
|
||||
flex-direction: column;
|
||||
}
|
||||
.message-tab-content-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.chat-object-view {
|
||||
display: none;
|
||||
}
|
||||
[showingchattools] :is(.thread-view, .chat-object-view) {
|
||||
overflow: scroll;
|
||||
position: absolute;
|
||||
padding: 0px;
|
||||
}
|
||||
[showingchattools] header {
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
[showingchattools] .chat-object-view {
|
||||
display: flex;
|
||||
top: var(--header-height);
|
||||
bottom: calc(var(--thread-view-height-with-tools) + var(--footer-top-line));
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
[showingchattools] .thread-view {
|
||||
display: flex;
|
||||
height: var(--thread-view-height-with-tools);
|
||||
top: unset;
|
||||
bottom: var(--footer-top-line);
|
||||
flex: 1;
|
||||
}
|
||||
@media (min-width: 640px) and (max-width: 1400px) {
|
||||
main.split:has([showingchattools]) {
|
||||
display: flex;
|
||||
}
|
||||
main.split:has([showingchattools]) #settings-tab {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@media (min-width: 640px) {
|
||||
[showingchattools] :is(.thread-view, .chat-object-view) {
|
||||
top: var(--header-height);
|
||||
bottom: var(--footer-top-line);
|
||||
}
|
||||
[showingchattools] .chat-object-view {
|
||||
left: 0px;
|
||||
right: var(--thread-view-width-with-tools);
|
||||
border-bottom: none;
|
||||
border-right: 1px solid var(--border);
|
||||
}
|
||||
[showingchattools] .thread-view {
|
||||
display: flex;
|
||||
left: unset;
|
||||
right: 0px;
|
||||
height: unset;
|
||||
width: var(--thread-view-width-with-tools);
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+528
-77
@@ -76,6 +76,9 @@
|
||||
this._bindings.add(fn);
|
||||
fn(this._value);
|
||||
}
|
||||
subscribeSilent(fn) {
|
||||
this._bindings.add(fn);
|
||||
}
|
||||
// stringify
|
||||
toString() {
|
||||
return JSON.stringify(this._value);
|
||||
@@ -118,7 +121,48 @@
|
||||
}
|
||||
// stringification
|
||||
toString() {
|
||||
const array = [...this.value];
|
||||
const array = [...this.value.values()];
|
||||
const json = JSON.stringify(array);
|
||||
return json;
|
||||
}
|
||||
};
|
||||
var MapState = class extends State {
|
||||
additionHandlers = /* @__PURE__ */ new Set();
|
||||
removalHandlers = /* @__PURE__ */ new Map();
|
||||
// init
|
||||
constructor(initialItems) {
|
||||
super(new Map(initialItems));
|
||||
}
|
||||
// list
|
||||
set(key, item) {
|
||||
this.remove(key);
|
||||
this.value.set(key, item);
|
||||
this.additionHandlers.forEach((handler) => handler(item));
|
||||
this.callSubscriptions();
|
||||
}
|
||||
remove(key) {
|
||||
const item = this.value.get(key);
|
||||
if (!item) return;
|
||||
this.value.delete(key);
|
||||
this.callSubscriptions();
|
||||
if (!this.removalHandlers.has(item)) return;
|
||||
this.removalHandlers.get(item)(item);
|
||||
this.removalHandlers.delete(item);
|
||||
}
|
||||
clear() {
|
||||
[...this.value.keys()].forEach((key) => this.remove(key));
|
||||
}
|
||||
// handlers
|
||||
handleAddition(handler) {
|
||||
this.additionHandlers.add(handler);
|
||||
[...this.value.values()].forEach(handler);
|
||||
}
|
||||
handleRemoval(item, handler) {
|
||||
this.removalHandlers.set(item, handler);
|
||||
}
|
||||
// stringification
|
||||
toString() {
|
||||
const array = [...this.value.entries()];
|
||||
const json = JSON.stringify(array);
|
||||
return json;
|
||||
}
|
||||
@@ -130,6 +174,9 @@
|
||||
);
|
||||
return proxyState;
|
||||
}
|
||||
function bulkSubscribe(statesToSubscibe, fn) {
|
||||
statesToSubscibe.forEach((state) => state.subscribeSilent(fn));
|
||||
}
|
||||
function persistState(localStorageKey, state) {
|
||||
state.subscribe(() => {
|
||||
const stringifiedValue = state.toString();
|
||||
@@ -143,9 +190,8 @@
|
||||
persistState(localStorageKey, state);
|
||||
return state;
|
||||
}
|
||||
function restoreListState(localStorageKey) {
|
||||
function restoreListState(localStorageKey, initialItems = []) {
|
||||
const storedString = localStorage.getItem(localStorageKey) ?? "";
|
||||
let initialItems = [];
|
||||
try {
|
||||
const array = JSON.parse(storedString);
|
||||
if (!Array.isArray(array)) throw "";
|
||||
@@ -156,6 +202,18 @@
|
||||
persistState(localStorageKey, state);
|
||||
return state;
|
||||
}
|
||||
function restoreMapState(localStorageKey, initialItems = []) {
|
||||
const storedString = localStorage.getItem(localStorageKey) ?? "";
|
||||
try {
|
||||
const array = JSON.parse(storedString);
|
||||
if (!Array.isArray(array)) throw "";
|
||||
initialItems = array;
|
||||
} catch {
|
||||
}
|
||||
const state = new MapState(initialItems);
|
||||
persistState(localStorageKey, state);
|
||||
return state;
|
||||
}
|
||||
function createElement(tagName, attributes = {}, ...children) {
|
||||
const element = document.createElement(tagName);
|
||||
if (attributes != null)
|
||||
@@ -243,7 +301,7 @@
|
||||
}
|
||||
});
|
||||
} catch {
|
||||
throw `error: cannot process subscribe:children directive because ListItemConverter is not defined. Usage: "subscribe:children={[list, converter]}"; you can find a more detailed example in the documentation`;
|
||||
throw `error: cannot process subscribe:children directive because StateItemConverter is not defined. Usage: "subscribe:children={[list, converter]}"; you can find a more detailed example in the documentation`;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -346,9 +404,15 @@
|
||||
messages(id) {
|
||||
return id + "messages";
|
||||
},
|
||||
objects(id) {
|
||||
return id + "items";
|
||||
},
|
||||
outbox(id) {
|
||||
return id + "outbox";
|
||||
},
|
||||
itemOutbox(id) {
|
||||
return id + "item-outbox";
|
||||
},
|
||||
composingMessage(id) {
|
||||
return id + "composing-message";
|
||||
}
|
||||
@@ -363,7 +427,9 @@
|
||||
secondaryChannels;
|
||||
encryptionKey;
|
||||
messages;
|
||||
objects;
|
||||
outbox;
|
||||
objectOutbox;
|
||||
isOutBoxEmpty;
|
||||
composingMessage;
|
||||
primaryChannelInput;
|
||||
@@ -374,6 +440,7 @@
|
||||
cannotSetChannel;
|
||||
cannotUndoChannel;
|
||||
cannotClearMessages;
|
||||
cannotClearObjects;
|
||||
// init
|
||||
constructor(id = UUID()) {
|
||||
this.id = id;
|
||||
@@ -390,7 +457,9 @@
|
||||
);
|
||||
this.encryptionKey = restoreState(storageKeys.encyptionKey(id), "");
|
||||
this.messages = restoreListState(storageKeys.messages(id));
|
||||
this.objects = restoreMapState(storageKeys.objects(id));
|
||||
this.outbox = restoreListState(storageKeys.outbox(id));
|
||||
this.objectOutbox = restoreMapState(storageKeys.itemOutbox(id));
|
||||
this.isOutBoxEmpty = createProxyState(
|
||||
[this.outbox],
|
||||
() => this.outbox.value.size == 0
|
||||
@@ -425,6 +494,10 @@
|
||||
[this.messages],
|
||||
() => this.messages.value.size == 0
|
||||
);
|
||||
this.cannotClearObjects = createProxyState(
|
||||
[this.objects],
|
||||
() => this.objects.value.size == 0
|
||||
);
|
||||
}
|
||||
// general
|
||||
deleteSelf = () => {
|
||||
@@ -441,7 +514,17 @@
|
||||
if (channels.indexOf(this.primaryChannel.value) == -1) return;
|
||||
if (data.subscribed != void 0) this.handleSubscription(data.subscribed);
|
||||
if (!data.messageBody) return;
|
||||
const { sender, body, channel, isoDate } = JSON.parse(data.messageBody);
|
||||
const message = JSON.parse(data.messageBody);
|
||||
const { sender, body, channel, isoDate, messageObjectString } = message;
|
||||
if (messageObjectString) {
|
||||
const decryptedMessageObjectString = await decryptString(
|
||||
messageObjectString,
|
||||
this.encryptionKey.value
|
||||
);
|
||||
const messageObject = JSON.parse(decryptedMessageObjectString);
|
||||
if (messageObject.id && messageObject.title)
|
||||
return this.handleMessageObject(messageObject);
|
||||
}
|
||||
this.handleMessage({
|
||||
sender,
|
||||
body: await decryptString(body, this.encryptionKey.value),
|
||||
@@ -459,21 +542,23 @@
|
||||
this.messages.add(chatMessage);
|
||||
if (selectedChat.value != this) this.hasUnreadMessages.value = true;
|
||||
};
|
||||
handleMessageObject = (messageObject) => {
|
||||
this.addObject(messageObject);
|
||||
};
|
||||
// sending
|
||||
sendMessagesInOutbox = () => {
|
||||
this.outbox.value.forEach(async (message) => {
|
||||
const isSent = await this.sendMessage(message);
|
||||
if (isSent == true) this.outbox.remove(message);
|
||||
});
|
||||
this.objectOutbox.value.forEach(async (messageObject) => {
|
||||
const chatMessage = await this.createChatMessage("", messageObject);
|
||||
const isSent = await this.sendMessage(chatMessage);
|
||||
if (isSent == true) this.objectOutbox.remove(messageObject.id);
|
||||
});
|
||||
};
|
||||
// messages
|
||||
sendNewMessage = async () => {
|
||||
if (this.cannotSendMessage.value == true) return;
|
||||
await this.sendMessageText(this.composingMessage.value);
|
||||
this.composingMessage.value = "";
|
||||
};
|
||||
sendMessageText = async (text) => {
|
||||
const message = await this.createMessage(text);
|
||||
this.sendExistingMessage(message);
|
||||
};
|
||||
resendMessage = (message) => {
|
||||
if (this.cannotResendMessage.value == true) return;
|
||||
this.sendMessageText(message.body);
|
||||
};
|
||||
createMessage = async (messageText) => {
|
||||
createChatMessage = async (messageText, messageObject) => {
|
||||
const secondaryChannelNames = [
|
||||
...this.secondaryChannels.value.values()
|
||||
];
|
||||
@@ -482,42 +567,128 @@
|
||||
...secondaryChannelNames
|
||||
];
|
||||
const joinedChannelName = allChannelNames.join("/");
|
||||
return {
|
||||
const chatMessage = {
|
||||
channel: joinedChannelName,
|
||||
sender: senderName.value,
|
||||
body: messageText,
|
||||
isoDate: (/* @__PURE__ */ new Date()).toISOString()
|
||||
};
|
||||
if (messageObject)
|
||||
chatMessage.messageObjectString = await encryptString(
|
||||
JSON.stringify(messageObject),
|
||||
this.encryptionKey.value
|
||||
);
|
||||
return chatMessage;
|
||||
};
|
||||
sendExistingMessage = async (chatMessage) => {
|
||||
if (isConnected.value == true && this.isSubscribed.value == true) {
|
||||
const encrypted = this.encryptionKey.value == "" ? chatMessage.body : await encryptString(chatMessage.body, this.encryptionKey.value);
|
||||
chatMessage.body = encrypted;
|
||||
const messageString = JSON.stringify(chatMessage);
|
||||
UDN.sendMessage(chatMessage.channel, messageString);
|
||||
} else {
|
||||
this.outbox.add(chatMessage);
|
||||
}
|
||||
sendMessageFromComposer = async () => {
|
||||
if (this.cannotSendMessage.value == true) return;
|
||||
await this.sendMessageFromText(this.composingMessage.value);
|
||||
this.composingMessage.value = "";
|
||||
};
|
||||
sendMessagesInOutbox = () => {
|
||||
this.outbox.value.forEach((message) => {
|
||||
this.sendExistingMessage(message);
|
||||
this.outbox.remove(message);
|
||||
});
|
||||
sendMessageFromText = async (text) => {
|
||||
const chatMessage = await this.createChatMessage(text);
|
||||
this.outbox.add(chatMessage);
|
||||
this.sendMessagesInOutbox();
|
||||
};
|
||||
resendMessage = (chatMessage) => {
|
||||
if (this.cannotResendMessage.value == true) return;
|
||||
this.sendMessageFromText(chatMessage.body);
|
||||
};
|
||||
clearMessages = () => {
|
||||
this.messages.clear();
|
||||
};
|
||||
deleteMessage = (message) => {
|
||||
this.messages.remove(message);
|
||||
deleteMessage = (chatMessage) => {
|
||||
this.messages.remove(chatMessage);
|
||||
};
|
||||
deleteOutboxMessage = (message) => {
|
||||
this.outbox.remove(message);
|
||||
deleteOutboxMessage = (chatMessage) => {
|
||||
this.outbox.remove(chatMessage);
|
||||
};
|
||||
decryptReceivedMessage = async (message) => {
|
||||
message.body = await decryptString(message.body, this.encryptionKey.value);
|
||||
decryptReceivedMessage = async (chatMessage) => {
|
||||
chatMessage.body = await decryptString(
|
||||
chatMessage.body,
|
||||
this.encryptionKey.value
|
||||
);
|
||||
this.messages.callSubscriptions();
|
||||
};
|
||||
sendMessage = async (chatMessage) => {
|
||||
if (isConnected.value == false || this.isSubscribed.value == false)
|
||||
return false;
|
||||
const encryptedBody = this.encryptionKey.value == "" ? chatMessage.body : await encryptString(chatMessage.body, this.encryptionKey.value);
|
||||
chatMessage.body = encryptedBody;
|
||||
const messageString = JSON.stringify(chatMessage);
|
||||
UDN.sendMessage(chatMessage.channel, messageString);
|
||||
return true;
|
||||
};
|
||||
// objects
|
||||
createObjectFromTitle = (title) => {
|
||||
const firstObjectContent = this.createObjectContent();
|
||||
const messageObject = {
|
||||
id: UUID(),
|
||||
title,
|
||||
contentVersions: {}
|
||||
};
|
||||
this.addObjectContent(messageObject, firstObjectContent);
|
||||
return messageObject;
|
||||
};
|
||||
createObjectContent = () => {
|
||||
return {
|
||||
id: UUID(),
|
||||
isoDateVersionCreated: (/* @__PURE__ */ new Date()).toISOString()
|
||||
};
|
||||
};
|
||||
addObjectContent = (messageObject, content) => {
|
||||
messageObject.contentVersions[content.id] = content;
|
||||
};
|
||||
addObject = (messageObject) => {
|
||||
const existingObject = this.objects.value.get(messageObject.id);
|
||||
if (existingObject) {
|
||||
existingObject.title = messageObject.title;
|
||||
Object.values(messageObject.contentVersions).forEach((content) => {
|
||||
this.addObjectContent(existingObject, content);
|
||||
});
|
||||
this.objects.set(existingObject.id, existingObject);
|
||||
} else {
|
||||
this.objects.set(messageObject.id, messageObject);
|
||||
}
|
||||
};
|
||||
addObjectAndSend = (messageObject) => {
|
||||
this.addObject(messageObject);
|
||||
this.sendObject(messageObject);
|
||||
};
|
||||
sendObject = (messageObject) => {
|
||||
this.objectOutbox.set(messageObject.id, messageObject);
|
||||
this.sendMessagesInOutbox();
|
||||
};
|
||||
deleteObject = (messageObject) => {
|
||||
this.objects.remove(messageObject.id);
|
||||
this.objectOutbox.remove(messageObject.id);
|
||||
};
|
||||
resendObjects = () => {
|
||||
this.objects.value.forEach((messageObject) => {
|
||||
this.sendObject(messageObject);
|
||||
});
|
||||
};
|
||||
clearObjects = () => {
|
||||
this.objects.clear();
|
||||
};
|
||||
getSortedContents = (messageObject) => {
|
||||
const contents = Object.values(messageObject.contentVersions);
|
||||
contents.sort(
|
||||
(a, b) => a.isoDateVersionCreated > b.isoDateVersionCreated ? 0 : 1
|
||||
);
|
||||
return contents;
|
||||
};
|
||||
getMostRecentContentId = (messageObject) => {
|
||||
const contents = Object.values(messageObject.contentVersions);
|
||||
return contents[contents.length - 1].id;
|
||||
};
|
||||
getMostRecentContent = (messageObject) => {
|
||||
const id = this.getMostRecentContentId(messageObject);
|
||||
return this.getObjectContentFromId(messageObject, id);
|
||||
};
|
||||
getObjectContentFromId = (messageObject, contentId) => {
|
||||
return messageObject.contentVersions[contentId];
|
||||
};
|
||||
// channel
|
||||
setChannel = () => {
|
||||
if (this.cannotSetChannel.value == true) return;
|
||||
@@ -755,6 +926,7 @@
|
||||
var chats = new ListState();
|
||||
var chatIds = restoreListState("chat-ids");
|
||||
var selectedChat = new State(void 0);
|
||||
var isShowingChatTools = new State(false);
|
||||
var newChatName = new State("");
|
||||
var cannotCreateChat = createProxyState(
|
||||
[newChatName],
|
||||
@@ -766,6 +938,7 @@
|
||||
newChatName.value = "";
|
||||
}
|
||||
function closeChatView() {
|
||||
isShowingChatTools.value = false;
|
||||
selectedChat.value = void 0;
|
||||
document.getElementById("settings-tab")?.scrollIntoView();
|
||||
}
|
||||
@@ -774,6 +947,9 @@
|
||||
chat.hasUnreadMessages.value = false;
|
||||
document.getElementById("message-tab")?.scrollIntoView();
|
||||
}
|
||||
function toggleChatTools() {
|
||||
isShowingChatTools.value = !isShowingChatTools.value;
|
||||
}
|
||||
chatIds.value.forEach((id) => chats.add(new Chat(id)));
|
||||
if (serverAddressInput.value != "" && didRequestConnection.value == true) {
|
||||
connect();
|
||||
@@ -783,9 +959,11 @@
|
||||
var englishTranslations = {
|
||||
// general
|
||||
set: "Set",
|
||||
save: "Save",
|
||||
back: "Back",
|
||||
undoChanges: "Undo changes",
|
||||
close: "Close",
|
||||
discard: "Discard",
|
||||
// overview
|
||||
overview: "Overview",
|
||||
connection: "Connection",
|
||||
@@ -797,7 +975,7 @@
|
||||
serverAddress: "Server Address",
|
||||
serverAddressPlaceholder: "wss://192.168.0.69:3000",
|
||||
connectToServer: "Connect",
|
||||
disconnect: "Disonnect",
|
||||
disconnect: "Disconnect",
|
||||
mailbox: "Mailbox",
|
||||
requestMailbox: "Enable",
|
||||
deleteMailbox: "Disable",
|
||||
@@ -805,7 +983,7 @@
|
||||
primaryChannel: "Primary channel",
|
||||
primaryChannelPlaceholder: "my-channel",
|
||||
addChat: "Add",
|
||||
// messages
|
||||
// chat
|
||||
showChatOptions: "show chat options",
|
||||
configureChatTitle: "Configure Chat",
|
||||
secondaryChannel: "Secondary channel",
|
||||
@@ -815,34 +993,53 @@
|
||||
encryptionKey: "Encryption key",
|
||||
encryptionKeyPlaceholder: "n10d2482dg283hg",
|
||||
showKey: "Show key",
|
||||
removeChat: "Remove chat",
|
||||
clearObjects: "Delete all objects",
|
||||
clearChatMessages: "Clear chat messages",
|
||||
removeChat: "Remove chat",
|
||||
messageInOutbox: "Pending",
|
||||
noChatSelected: "No chat selected",
|
||||
composerPlaceholder: "Type a message...",
|
||||
sendMessage: "Send",
|
||||
// messages
|
||||
resendMessage: "Resend message",
|
||||
decryptMessage: "Decrypt message",
|
||||
copyMessage: "Copy message",
|
||||
deleteMessage: "Delete message"
|
||||
deleteMessage: "Delete message",
|
||||
// objects
|
||||
showObjects: "show objects",
|
||||
createObject: "create object",
|
||||
untitledObject: "Untitled Object",
|
||||
viewAll: "All",
|
||||
viewNotes: "Notes",
|
||||
noObjects: "No objects",
|
||||
noNotes: "No notes",
|
||||
objectTitle: "Object title",
|
||||
objectTitlePlaceholder: "My object",
|
||||
objectVersion: "Object version",
|
||||
noteContent: "Note",
|
||||
noteContentPlaceholder: "Take a note...",
|
||||
resendObjects: "Resend all objects",
|
||||
deleteObject: "Delete object"
|
||||
};
|
||||
var allTranslations = {
|
||||
en: englishTranslations,
|
||||
es: {
|
||||
// general
|
||||
set: "Guardar",
|
||||
save: "Guardar",
|
||||
back: "Atr\xE1s",
|
||||
undoChanges: "Deshacer",
|
||||
close: "Cerrar",
|
||||
discard: "Descartar",
|
||||
// overview
|
||||
overview: "Resumen",
|
||||
connection: "Conexion",
|
||||
connection: "Conexi\xF3n",
|
||||
chats: "Chats",
|
||||
yourName: "Tu nombre",
|
||||
namePlaceholder: "Juan P\xE9rez",
|
||||
encryptionUnavailableTitle: "Cifrado no disponible",
|
||||
encryptionUnavailableMessage: "Obt\xE9n esta aplicaci\xF3n a traves de HTTPS o contin\xFAa sin cifrado",
|
||||
serverAddress: "Direccion del servidor",
|
||||
serverAddress: "Direcci\xF3n del servidor",
|
||||
serverAddressPlaceholder: "wss://192.168.0.69:3000",
|
||||
connectToServer: "Conectar",
|
||||
disconnect: "Desconectar",
|
||||
@@ -853,33 +1050,48 @@
|
||||
primaryChannel: "Canal principal",
|
||||
primaryChannelPlaceholder: "mi-canal",
|
||||
addChat: "A\xF1adir",
|
||||
// messages
|
||||
// chat
|
||||
showChatOptions: "Mostrar opciones del chat",
|
||||
configureChatTitle: "Configurar chat",
|
||||
secondaryChannel: "Canal segundario",
|
||||
secondaryChannelPlaceholder: "A\xF1adir canal segundario",
|
||||
addSecondaryChannel: "A\xF1adir canal segundario",
|
||||
removeSecondaryChannel: "Eliminar canal segundario",
|
||||
secondaryChannel: "Canal secundario",
|
||||
secondaryChannelPlaceholder: "A\xF1adir canal secundario",
|
||||
addSecondaryChannel: "A\xF1adir canal secundario",
|
||||
removeSecondaryChannel: "Eliminar canal secundario",
|
||||
encryptionKey: "Clave de cifrado",
|
||||
encryptionKeyPlaceholder: "n10d2482dg283hg",
|
||||
showKey: "Mostrar clave",
|
||||
clearObjects: "Eliminar todos los objetos",
|
||||
clearChatMessages: "Eliminar todos los mensajes",
|
||||
removeChat: "Eliminar chat",
|
||||
clearChatMessages: "Eliminar todos mensajes",
|
||||
noChatSelected: "Selecciona un chat",
|
||||
messageInOutbox: "Pendiente",
|
||||
noChatSelected: "Selecciona un chat",
|
||||
composerPlaceholder: "Escribe un mensaje...",
|
||||
sendMessage: "Enviar",
|
||||
resendMessage: "Enviar de nuevo",
|
||||
// messages
|
||||
resendMessage: "Reenviar mensaje",
|
||||
decryptMessage: "Descifrar mensaje",
|
||||
copyMessage: "Copiar mensaje",
|
||||
deleteMessage: "Eliminar este mensaje"
|
||||
deleteMessage: "Eliminar este mensaje",
|
||||
// objects
|
||||
showObjects: "mostrar objetos",
|
||||
createObject: "a\xF1adir nuevo objeto",
|
||||
untitledObject: "Sin T\xEDtulo",
|
||||
objectTitle: "T\xEDtulo",
|
||||
objectTitlePlaceholder: "Mi objeto",
|
||||
objectVersion: "Version del objeto",
|
||||
noteContent: "Nota",
|
||||
noteContentPlaceholder: "Toma nota...",
|
||||
resendObjects: "Reenviar todos objetos",
|
||||
deleteObject: "Eliminar objeto"
|
||||
},
|
||||
de: {
|
||||
// general
|
||||
set: "OK",
|
||||
save: "Sichern",
|
||||
back: "Zur\xFCck",
|
||||
undoChanges: "\xC4nderungen verwerfen",
|
||||
close: "Schlie\xDFen",
|
||||
discard: "Verwerfen",
|
||||
// overview
|
||||
overview: "\xDCbersicht",
|
||||
connection: "Verbindung",
|
||||
@@ -899,7 +1111,7 @@
|
||||
primaryChannel: "Hauptkanal",
|
||||
primaryChannelPlaceholder: "mein-kanal",
|
||||
addChat: "Hinzuf\xFCgen",
|
||||
// messages
|
||||
// chat
|
||||
showChatOptions: "Chatoptionen einblenden",
|
||||
configureChatTitle: "Chat konfigurieren",
|
||||
secondaryChannel: "Zweitkanal",
|
||||
@@ -909,22 +1121,232 @@
|
||||
encryptionKey: "Schl\xFCssel",
|
||||
encryptionKeyPlaceholder: "n10d2482dg283hg",
|
||||
showKey: "Schl\xFCssel anzeigen",
|
||||
removeChat: "Chat l\xF6schen",
|
||||
clearObjects: "Alle Objekte l\xF6schen",
|
||||
clearChatMessages: "Nachrichtenverlauf leeren",
|
||||
noChatSelected: "Kein Chat ausgew\xE4hlt",
|
||||
removeChat: "Chat l\xF6schen",
|
||||
messageInOutbox: "Ausstehend",
|
||||
noChatSelected: "Kein Chat ausgew\xE4hlt",
|
||||
composerPlaceholder: "Neue Nachricht...",
|
||||
sendMessage: "Senden",
|
||||
// messages
|
||||
resendMessage: "Erneut senden",
|
||||
decryptMessage: "Nachricht entschl\xFCsseln",
|
||||
copyMessage: "Nachricht kopieren",
|
||||
deleteMessage: "Nachricht l\xF6schen"
|
||||
deleteMessage: "Nachricht l\xF6schen",
|
||||
// objects
|
||||
showObjects: "Objekte anzeigen",
|
||||
createObject: "Neues Objekt erstellen",
|
||||
untitledObject: "Unbenannt",
|
||||
objectTitle: "Titel",
|
||||
objectTitlePlaceholder: "Mein Objekt",
|
||||
objectVersion: "Version des Objekts",
|
||||
noteContent: "Notiz",
|
||||
noteContentPlaceholder: "Notiz eingeben...",
|
||||
resendObjects: "Objekte erneut senden",
|
||||
deleteObject: "Objekt l\xF6schen"
|
||||
}
|
||||
};
|
||||
var language = navigator.language.substring(0, 2);
|
||||
var translation = allTranslations[language] ?? allTranslations.en;
|
||||
|
||||
// src/Views/chatOptionModal.tsx
|
||||
// src/Views/Objects/allObjectsView.tsx
|
||||
function AllObjectsView(chat, selectedObject, isShowingObjectModal) {
|
||||
const objectConverter = (messageObject) => {
|
||||
function select() {
|
||||
selectedObject.value = messageObject;
|
||||
isShowingObjectModal.value = true;
|
||||
}
|
||||
return /* @__PURE__ */ createElement("button", { class: "tile", "on:click": select }, /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("b", null, messageObject.title), /* @__PURE__ */ createElement("span", { class: "secondary" }, messageObject.id)));
|
||||
};
|
||||
const content = createProxyState(
|
||||
[chat.objects],
|
||||
() => chat.objects.value.size == 0 ? /* @__PURE__ */ createElement("div", { class: "flex-column width-100 height-100 align-center justify-center secondary" }, translation.noObjects) : /* @__PURE__ */ createElement(
|
||||
"div",
|
||||
{
|
||||
class: "grid gap padding",
|
||||
style: "grid-template-columns: repeat(auto-fill, minmax(350px, 1fr))",
|
||||
"children:prepend": [chat.objects, objectConverter]
|
||||
}
|
||||
)
|
||||
);
|
||||
return /* @__PURE__ */ createElement("div", { class: "width-100 height-100", "children:set": content });
|
||||
}
|
||||
|
||||
// src/Views/Objects/noteObjectsView.tsx
|
||||
function NoteObjectsView(chat, selectedObject, isShowingObjectModal) {
|
||||
const objectConverter = (messageObject) => {
|
||||
const latest = chat.getMostRecentContent(messageObject);
|
||||
function select() {
|
||||
selectedObject.value = messageObject;
|
||||
isShowingObjectModal.value = true;
|
||||
}
|
||||
return /* @__PURE__ */ createElement("button", { class: "tile", "on:click": select }, /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("b", null, messageObject.title), /* @__PURE__ */ createElement("span", { class: "secondary ellipsis" }, latest.noteContent.split("\n")[0])));
|
||||
};
|
||||
const notes = new ListState();
|
||||
chat.objects.subscribe(() => {
|
||||
notes.clear();
|
||||
chat.objects.value.forEach((messageObject) => {
|
||||
const latest = chat.getMostRecentContent(messageObject);
|
||||
if (!latest.noteContent) return;
|
||||
notes.add(messageObject);
|
||||
});
|
||||
});
|
||||
const content = createProxyState(
|
||||
[chat.objects],
|
||||
() => notes.value.size == 0 ? /* @__PURE__ */ createElement("div", { class: "flex-column width-100 height-100 align-center justify-center secondary" }, translation.noNotes) : /* @__PURE__ */ createElement(
|
||||
"div",
|
||||
{
|
||||
class: "grid gap padding",
|
||||
style: "grid-template-columns: repeat(auto-fill, minmax(350px, 1fr))",
|
||||
"children:prepend": [notes, objectConverter]
|
||||
}
|
||||
)
|
||||
);
|
||||
return /* @__PURE__ */ createElement("div", { class: "width-100 height-100", "children:set": content });
|
||||
}
|
||||
|
||||
// src/Views/Objects/objectDetailModal.tsx
|
||||
function ObjectDetailModal(chat, messageObject, isPresented) {
|
||||
const editingTitle = new State(messageObject.title);
|
||||
const selectedMessageObjectId = new State(
|
||||
chat.getMostRecentContentId(messageObject)
|
||||
);
|
||||
const selectedMessageObject = createProxyState(
|
||||
[selectedMessageObjectId],
|
||||
() => chat.getObjectContentFromId(messageObject, selectedMessageObjectId.value)
|
||||
);
|
||||
const didEditContent = new State(false);
|
||||
const editingNoteContent = createProxyState(
|
||||
[selectedMessageObject],
|
||||
() => selectedMessageObject.value.noteContent ?? ""
|
||||
);
|
||||
bulkSubscribe(
|
||||
[editingNoteContent],
|
||||
() => didEditContent.value = true
|
||||
);
|
||||
function handleKeyDown(e) {
|
||||
if (!e.metaKey && !e.ctrlKey) return;
|
||||
switch (e.key) {
|
||||
case "s":
|
||||
e.preventDefault();
|
||||
saveAndClose();
|
||||
break;
|
||||
}
|
||||
}
|
||||
function closeModal() {
|
||||
isPresented.value = false;
|
||||
}
|
||||
function saveAndClose() {
|
||||
messageObject.title = editingTitle.value;
|
||||
if (didEditContent.value == true) {
|
||||
chat.addObjectContent(messageObject, {
|
||||
isoDateVersionCreated: (/* @__PURE__ */ new Date()).toISOString(),
|
||||
id: UUID(),
|
||||
noteContent: editingNoteContent.value
|
||||
});
|
||||
}
|
||||
chat.addObjectAndSend(messageObject);
|
||||
closeModal();
|
||||
}
|
||||
function deleteAndClose() {
|
||||
chat.deleteObject(messageObject);
|
||||
closeModal();
|
||||
}
|
||||
return /* @__PURE__ */ createElement("div", { class: "modal", "toggle:open": isPresented, "on:keydown": handleKeyDown }, /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("main", null, /* @__PURE__ */ createElement("h2", null, messageObject.title), /* @__PURE__ */ createElement("span", { class: "secondary" }, messageObject.id), /* @__PURE__ */ createElement("hr", null), /* @__PURE__ */ createElement("div", { class: "flex-column gap" }, /* @__PURE__ */ createElement("label", { class: "tile" }, /* @__PURE__ */ createElement("span", { class: "icon" }, "label"), /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("span", null, translation.objectTitle), /* @__PURE__ */ createElement(
|
||||
"input",
|
||||
{
|
||||
autofocus: true,
|
||||
"bind:value": editingTitle,
|
||||
placeholder: translation.objectTitlePlaceholder
|
||||
}
|
||||
))), /* @__PURE__ */ createElement("hr", null), /* @__PURE__ */ createElement("label", { class: "tile" }, /* @__PURE__ */ createElement("span", { class: "icon" }, "history"), /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("span", null, translation.objectVersion), /* @__PURE__ */ createElement("select", { "bind:value": selectedMessageObjectId }, ...chat.getSortedContents(messageObject).map((content) => /* @__PURE__ */ createElement("option", { value: content.id }, new Date(
|
||||
content.isoDateVersionCreated
|
||||
).toLocaleString()))), /* @__PURE__ */ createElement("span", { class: "icon" }, "arrow_drop_down"))), /* @__PURE__ */ createElement("label", { class: "tile" }, /* @__PURE__ */ createElement("span", { class: "icon" }, "sticky_note_2"), /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("span", null, translation.noteContent), /* @__PURE__ */ createElement(
|
||||
"textarea",
|
||||
{
|
||||
rows: "5",
|
||||
"bind:value": editingNoteContent,
|
||||
placeholder: translation.noteContentPlaceholder
|
||||
}
|
||||
))), /* @__PURE__ */ createElement("hr", null), /* @__PURE__ */ createElement("button", { class: "danger width-input", "on:click": deleteAndClose }, translation.deleteObject, /* @__PURE__ */ createElement("span", { class: "icon" }, "delete")))), /* @__PURE__ */ createElement("div", { class: "flex-row" }, /* @__PURE__ */ createElement("button", { class: "flex-1 width-100 danger", "on:click": closeModal }, translation.discard), /* @__PURE__ */ createElement("button", { class: "flex-1 width-100 primary", "on:click": saveAndClose }, translation.save, /* @__PURE__ */ createElement("span", { class: "icon" }, "save")))));
|
||||
}
|
||||
|
||||
// src/Views/Objects/chatObjectView.tsx
|
||||
var viewTypes = {
|
||||
all: [translation.viewAll, "grid_view"],
|
||||
notes: [translation.viewNotes, "sticky_note_2"]
|
||||
};
|
||||
function ChatObjectView(chat) {
|
||||
const isShowingObjectModal = new State(false);
|
||||
const selectedObject = new State(void 0);
|
||||
const objectModal = createProxyState(
|
||||
[chat.objects, selectedObject],
|
||||
() => {
|
||||
if (selectedObject.value == void 0) return /* @__PURE__ */ createElement("div", null);
|
||||
return ObjectDetailModal(
|
||||
chat,
|
||||
selectedObject.value,
|
||||
isShowingObjectModal
|
||||
);
|
||||
}
|
||||
);
|
||||
const selectedViewType = new State("all");
|
||||
const mainView = createProxyState([selectedViewType], () => {
|
||||
function getViewFunction() {
|
||||
switch (selectedViewType.value) {
|
||||
case "notes":
|
||||
return NoteObjectsView;
|
||||
default:
|
||||
return AllObjectsView;
|
||||
}
|
||||
}
|
||||
return getViewFunction()(chat, selectedObject, isShowingObjectModal);
|
||||
});
|
||||
function createObject() {
|
||||
const newObject = chat.createObjectFromTitle(translation.untitledObject);
|
||||
chat.addObjectAndSend(newObject);
|
||||
selectedObject.value = newObject;
|
||||
isShowingObjectModal.value = true;
|
||||
}
|
||||
return /* @__PURE__ */ createElement("div", { class: "chat-object-view flex-column" }, /* @__PURE__ */ createElement("div", { class: "flex-row align-center border-bottom" }, /* @__PURE__ */ createElement(
|
||||
"button",
|
||||
{
|
||||
class: "primary height-100",
|
||||
"on:click": createObject,
|
||||
"aria-label": translation.createObject
|
||||
},
|
||||
/* @__PURE__ */ createElement("span", { class: "icon" }, "add")
|
||||
), /* @__PURE__ */ createElement("div", { class: "padding-sm flex flex-row gap justify-center scroll-h width-100" }, ...Object.keys(viewTypes).map(
|
||||
(key) => ViewTypeToggle(key, selectedViewType)
|
||||
)), /* @__PURE__ */ createElement(
|
||||
"button",
|
||||
{
|
||||
class: "height-100",
|
||||
"on:click": createObject,
|
||||
"aria-label": translation.createObject
|
||||
},
|
||||
/* @__PURE__ */ createElement("span", { class: "icon" }, "visibility")
|
||||
)), /* @__PURE__ */ createElement(
|
||||
"div",
|
||||
{
|
||||
class: "width-100 height-100 flex-1 scroll-h",
|
||||
"children:set": mainView
|
||||
}
|
||||
), /* @__PURE__ */ createElement("div", { "children:set": objectModal }));
|
||||
}
|
||||
function ViewTypeToggle(key, selection) {
|
||||
const [label, icon] = viewTypes[key];
|
||||
function select() {
|
||||
selection.value = key;
|
||||
}
|
||||
const isSelected = createProxyState(
|
||||
[selection],
|
||||
() => selection.value == key
|
||||
);
|
||||
return /* @__PURE__ */ createElement("button", { "aria-label": label, "on:click": select, "toggle:selected": isSelected }, /* @__PURE__ */ createElement("span", { class: "icon" }, icon));
|
||||
}
|
||||
|
||||
// src/Views/Chat/chatOptionModal.tsx
|
||||
function ChatOptionModal(chat, isPresented) {
|
||||
function closeModal() {
|
||||
isPresented.value = false;
|
||||
@@ -1011,7 +1433,16 @@
|
||||
"bind:value": chat.encryptionKey,
|
||||
"set:type": inputType
|
||||
}
|
||||
))), /* @__PURE__ */ createElement("label", { class: "inline margin-0" }, /* @__PURE__ */ createElement("input", { type: "checkbox", "bind:checked": shouldShowKey }), translation.showKey)), /* @__PURE__ */ createElement("hr", null), /* @__PURE__ */ createElement("div", { class: "flex-column gap" }, /* @__PURE__ */ createElement(
|
||||
))), /* @__PURE__ */ createElement("label", { class: "inline margin-0" }, /* @__PURE__ */ createElement("input", { type: "checkbox", "bind:checked": shouldShowKey }), translation.showKey)), /* @__PURE__ */ createElement("hr", null), /* @__PURE__ */ createElement("div", { class: "flex-column gap width-input" }, /* @__PURE__ */ createElement("button", { "on:click": chat.resendObjects }, translation.resendObjects, /* @__PURE__ */ createElement("span", { class: "icon" }, "replay"))), /* @__PURE__ */ createElement("hr", null), /* @__PURE__ */ createElement("div", { class: "flex-column gap width-input" }, /* @__PURE__ */ createElement(
|
||||
"button",
|
||||
{
|
||||
class: "danger",
|
||||
"on:click": chat.clearObjects,
|
||||
"toggle:disabled": chat.cannotClearObjects
|
||||
},
|
||||
translation.clearObjects,
|
||||
/* @__PURE__ */ createElement("span", { class: "icon" }, "deployed_code")
|
||||
), /* @__PURE__ */ createElement(
|
||||
"button",
|
||||
{
|
||||
class: "danger",
|
||||
@@ -1019,11 +1450,11 @@
|
||||
"toggle:disabled": chat.cannotClearMessages
|
||||
},
|
||||
translation.clearChatMessages,
|
||||
/* @__PURE__ */ createElement("span", { class: "icon" }, "delete_sweep")
|
||||
), /* @__PURE__ */ createElement("button", { class: "danger", "on:click": deleteChat }, translation.removeChat, /* @__PURE__ */ createElement("span", { class: "icon" }, "delete")))), /* @__PURE__ */ createElement("button", { "on:click": closeModal }, translation.close, /* @__PURE__ */ createElement("span", { class: "icon" }, "close"))));
|
||||
/* @__PURE__ */ createElement("span", { class: "icon" }, "chat_error")
|
||||
), /* @__PURE__ */ createElement("button", { class: "danger", "on:click": deleteChat }, translation.removeChat, /* @__PURE__ */ createElement("span", { class: "icon" }, "delete_forever")))), /* @__PURE__ */ createElement("button", { "on:click": closeModal }, translation.close, /* @__PURE__ */ createElement("span", { class: "icon" }, "close"))));
|
||||
}
|
||||
|
||||
// src/Views/messageComposer.tsx
|
||||
// src/Views/Chat/messageComposer.tsx
|
||||
function MessageComposer(chat) {
|
||||
return /* @__PURE__ */ createElement("div", { class: "flex-row width-100" }, " ", /* @__PURE__ */ createElement(
|
||||
"input",
|
||||
@@ -1032,20 +1463,20 @@
|
||||
style: "max-width: unset",
|
||||
placeholder: translation.composerPlaceholder,
|
||||
"bind:value": chat.composingMessage,
|
||||
"on:enter": chat.sendNewMessage
|
||||
"on:enter": chat.sendMessageFromComposer
|
||||
}
|
||||
), /* @__PURE__ */ createElement(
|
||||
"button",
|
||||
{
|
||||
class: "primary",
|
||||
"on:click": chat.sendNewMessage,
|
||||
"on:click": chat.sendMessageFromComposer,
|
||||
"toggle:disabled": chat.cannotSendMessage
|
||||
},
|
||||
/* @__PURE__ */ createElement("span", { class: "icon" }, "send")
|
||||
));
|
||||
}
|
||||
|
||||
// src/Views/threadView.tsx
|
||||
// src/Views/Chat/threadView.tsx
|
||||
function ThreadView(chat) {
|
||||
const messageConverter = (message) => {
|
||||
function resendMessage() {
|
||||
@@ -1108,15 +1539,19 @@
|
||||
"children:append": [chat.outbox, outboxMessageConverter]
|
||||
}
|
||||
);
|
||||
const listWrapper = /* @__PURE__ */ createElement("div", { class: "flex-column gap" }, messageList, outboxList);
|
||||
const listWrapper = /* @__PURE__ */ createElement("div", { class: "thread-view flex-column gap" }, messageList, outboxList);
|
||||
function scrollToBottom() {
|
||||
const scrollFromBottom = listWrapper.scrollHeight - (listWrapper.scrollTop + listWrapper.offsetHeight);
|
||||
if (scrollFromBottom > 400) return;
|
||||
listWrapper.scrollTop = listWrapper.scrollHeight;
|
||||
}
|
||||
chat.messages.handleAddition(scrollToBottom);
|
||||
chat.outbox.handleAddition(scrollToBottom);
|
||||
setTimeout(() => listWrapper.scrollTop = listWrapper.scrollHeight, 50);
|
||||
function scrollToBottomIfAppropriate() {
|
||||
const scrollFromBottom = listWrapper.scrollHeight - (listWrapper.scrollTop + listWrapper.offsetHeight);
|
||||
if (scrollFromBottom > 400) return;
|
||||
scrollToBottom();
|
||||
}
|
||||
chat.messages.handleAddition(scrollToBottomIfAppropriate);
|
||||
chat.outbox.handleAddition(scrollToBottomIfAppropriate);
|
||||
isShowingChatTools.subscribe(() => scrollToBottom());
|
||||
setTimeout(() => scrollToBottom(), 50);
|
||||
return listWrapper;
|
||||
}
|
||||
|
||||
@@ -1132,6 +1567,14 @@
|
||||
}
|
||||
return [
|
||||
/* @__PURE__ */ createElement("header", { class: "padding-0" }, /* @__PURE__ */ createElement("span", { class: "flex-row align-center" }, /* @__PURE__ */ createElement("button", { "aria-label": translation.back, "on:click": closeChatView }, /* @__PURE__ */ createElement("span", { class: "icon" }, "arrow_back")), /* @__PURE__ */ createElement("span", { "subscribe:innerText": chat.primaryChannel })), /* @__PURE__ */ createElement("span", null, /* @__PURE__ */ createElement(
|
||||
"button",
|
||||
{
|
||||
"aria-label": translation.showObjects,
|
||||
"on:click": toggleChatTools,
|
||||
"toggle:selected": isShowingChatTools
|
||||
},
|
||||
/* @__PURE__ */ createElement("span", { class: "icon" }, "deployed_code")
|
||||
), /* @__PURE__ */ createElement(
|
||||
"button",
|
||||
{
|
||||
"aria-label": translation.showChatOptions,
|
||||
@@ -1139,15 +1582,23 @@
|
||||
},
|
||||
/* @__PURE__ */ createElement("span", { class: "icon" }, "tune")
|
||||
))),
|
||||
ChatObjectView(chat),
|
||||
ThreadView(chat),
|
||||
/* @__PURE__ */ createElement("footer", null, MessageComposer(chat)),
|
||||
ChatOptionModal(chat, isShowingOptions)
|
||||
];
|
||||
});
|
||||
return /* @__PURE__ */ createElement("article", { id: "message-tab", "children:set": messageTabContent });
|
||||
return /* @__PURE__ */ createElement(
|
||||
"article",
|
||||
{
|
||||
"toggle:showingchattools": isShowingChatTools,
|
||||
id: "message-tab",
|
||||
"children:set": messageTabContent
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// src/Views/chatListSection.tsx
|
||||
// src/Views/Overview/chatListSection.tsx
|
||||
var chatConverter = (chat) => {
|
||||
function select() {
|
||||
selectChat(chat);
|
||||
@@ -1194,7 +1645,7 @@
|
||||
));
|
||||
}
|
||||
|
||||
// src/Views/connectionSection.tsx
|
||||
// src/Views/Overview/connectionSection.tsx
|
||||
var addressConverter = (address) => {
|
||||
return /* @__PURE__ */ createElement("option", null, address);
|
||||
};
|
||||
@@ -1261,7 +1712,7 @@
|
||||
)));
|
||||
}
|
||||
|
||||
// src/Views/personalSection.tsx
|
||||
// src/Views/Overview/personalSection.tsx
|
||||
function PersonalSection() {
|
||||
return /* @__PURE__ */ createElement("div", { class: "flex-column" }, /* @__PURE__ */ createElement("label", { class: "tile" }, /* @__PURE__ */ createElement("span", { class: "icon" }, "account_circle"), /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("span", null, translation.yourName), /* @__PURE__ */ createElement(
|
||||
"input",
|
||||
|
||||
Vendored
+30
-6
@@ -140,10 +140,12 @@ select,
|
||||
@media (hover: hover) {
|
||||
button:hover,
|
||||
select:hover,
|
||||
button.tile.ghost:hover,
|
||||
[selected]:active {
|
||||
button.tile.ghost:hover {
|
||||
background-color: var(--surface-hover);
|
||||
}
|
||||
[selected]:hover {
|
||||
background-color: var(--surface);
|
||||
}
|
||||
}
|
||||
button:active,
|
||||
select:active,
|
||||
@@ -151,6 +153,9 @@ button.tile.ghost:active,
|
||||
[selected] {
|
||||
background-color: var(--surface-active);
|
||||
}
|
||||
[selected]:active {
|
||||
background-color: var(--surface-hover);
|
||||
}
|
||||
header button {
|
||||
background-color: transparent;
|
||||
}
|
||||
@@ -318,6 +323,10 @@ hr {
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.border-bottom {
|
||||
border-bottom: 1px solid var(--border) !important;
|
||||
}
|
||||
|
||||
input:not([type="checkbox"]):not([type="radio"]):not([type="range"]):focus:not(
|
||||
label.tile *
|
||||
),
|
||||
@@ -480,7 +489,7 @@ input[type="radio"],
|
||||
}
|
||||
|
||||
.justify-center {
|
||||
justify-content: center !important;
|
||||
justify-content: safe center !important;
|
||||
}
|
||||
|
||||
.justify-end {
|
||||
@@ -577,6 +586,10 @@ textarea,
|
||||
padding: var(--control-padding) !important;
|
||||
}
|
||||
|
||||
.padding-sm {
|
||||
padding: var(--gap-between-tiles) !important;
|
||||
}
|
||||
|
||||
.modal main {
|
||||
padding: var(--modal-padding);
|
||||
}
|
||||
@@ -597,7 +610,7 @@ label.tile:has(> span.icon) {
|
||||
padding-left: var(--control-padding);
|
||||
}
|
||||
|
||||
label.tile:has(> span.icon) input {
|
||||
label.tile:has(> span.icon) :is(input, select, textarea) {
|
||||
padding-left: 0px;
|
||||
}
|
||||
|
||||
@@ -730,8 +743,7 @@ button:not(.tile):not(header *),
|
||||
footer,
|
||||
input:not([type="checkbox"]):not([type="radio"]),
|
||||
select,
|
||||
select + .icon,
|
||||
textarea {
|
||||
select + .icon {
|
||||
height: var(--control-height);
|
||||
}
|
||||
|
||||
@@ -781,6 +793,10 @@ input[type="radio"] {
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
label.tile textarea {
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
/*
|
||||
text
|
||||
*/
|
||||
@@ -952,6 +968,14 @@ main {
|
||||
overflow-x: scroll;
|
||||
}
|
||||
|
||||
.scroll-h {
|
||||
overflow-x: scroll !important;
|
||||
}
|
||||
|
||||
.scroll-v {
|
||||
overflow-y: scroll !important;
|
||||
}
|
||||
|
||||
.ellipsis {
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"bloatless-react": "^1.3.1-beta1",
|
||||
"bloatless-react": "^1.3.7",
|
||||
"esbuild": "latest",
|
||||
"udn-frontend": "^1.0.7-beta1"
|
||||
},
|
||||
|
||||
+198
-45
@@ -21,6 +21,22 @@ export interface ChatMessage {
|
||||
sender: string;
|
||||
body: string;
|
||||
isoDate: string;
|
||||
|
||||
messageObjectString?: string;
|
||||
}
|
||||
|
||||
// object
|
||||
export interface MessageObject {
|
||||
id: string;
|
||||
title: string;
|
||||
contentVersions: { [key: string]: MessageObjectContent };
|
||||
}
|
||||
|
||||
export interface MessageObjectContent {
|
||||
isoDateVersionCreated: string;
|
||||
id: string;
|
||||
|
||||
noteContent?: string;
|
||||
}
|
||||
|
||||
// chat
|
||||
@@ -34,7 +50,9 @@ export class Chat {
|
||||
|
||||
encryptionKey: React.State<string>;
|
||||
messages: React.ListState<ChatMessage>;
|
||||
objects: React.MapState<MessageObject>;
|
||||
outbox: React.ListState<ChatMessage>;
|
||||
objectOutbox: React.MapState<MessageObject>;
|
||||
isOutBoxEmpty: React.State<boolean>;
|
||||
|
||||
composingMessage: React.State<string>;
|
||||
@@ -47,6 +65,7 @@ export class Chat {
|
||||
cannotSetChannel: React.State<boolean>;
|
||||
cannotUndoChannel: React.State<boolean>;
|
||||
cannotClearMessages: React.State<boolean>;
|
||||
cannotClearObjects: React.State<boolean>;
|
||||
|
||||
// init
|
||||
constructor(id: string = React.UUID()) {
|
||||
@@ -68,7 +87,9 @@ export class Chat {
|
||||
);
|
||||
this.encryptionKey = React.restoreState(storageKeys.encyptionKey(id), "");
|
||||
this.messages = React.restoreListState(storageKeys.messages(id));
|
||||
this.objects = React.restoreMapState(storageKeys.objects(id));
|
||||
this.outbox = React.restoreListState(storageKeys.outbox(id));
|
||||
this.objectOutbox = React.restoreMapState(storageKeys.itemOutbox(id));
|
||||
this.isOutBoxEmpty = React.createProxyState(
|
||||
[this.outbox],
|
||||
() => this.outbox.value.size == 0
|
||||
@@ -112,6 +133,10 @@ export class Chat {
|
||||
[this.messages],
|
||||
() => this.messages.value.size == 0
|
||||
);
|
||||
this.cannotClearObjects = React.createProxyState(
|
||||
[this.objects],
|
||||
() => this.objects.value.size == 0
|
||||
);
|
||||
}
|
||||
|
||||
// general
|
||||
@@ -131,9 +156,21 @@ export class Chat {
|
||||
if (channels.indexOf(this.primaryChannel.value) == -1) return;
|
||||
|
||||
if (data.subscribed != undefined) this.handleSubscription(data.subscribed);
|
||||
|
||||
if (!data.messageBody) return;
|
||||
const { sender, body, channel, isoDate } = JSON.parse(data.messageBody);
|
||||
|
||||
const message: ChatMessage = JSON.parse(data.messageBody);
|
||||
const { sender, body, channel, isoDate, messageObjectString } = message;
|
||||
|
||||
if (messageObjectString) {
|
||||
const decryptedMessageObjectString = await decryptString(
|
||||
messageObjectString,
|
||||
this.encryptionKey.value
|
||||
);
|
||||
const messageObject = JSON.parse(decryptedMessageObjectString);
|
||||
if (messageObject.id && messageObject.title)
|
||||
return this.handleMessageObject(messageObject);
|
||||
}
|
||||
|
||||
this.handleMessage({
|
||||
sender,
|
||||
body: await decryptString(body, this.encryptionKey.value),
|
||||
@@ -155,25 +192,28 @@ export class Chat {
|
||||
if (selectedChat.value != this) this.hasUnreadMessages.value = true;
|
||||
};
|
||||
|
||||
handleMessageObject = (messageObject: MessageObject): void => {
|
||||
this.addObject(messageObject);
|
||||
};
|
||||
|
||||
// sending
|
||||
sendMessagesInOutbox = () => {
|
||||
this.outbox.value.forEach(async (message) => {
|
||||
const isSent = await this.sendMessage(message);
|
||||
if (isSent == true) this.outbox.remove(message);
|
||||
});
|
||||
this.objectOutbox.value.forEach(async (messageObject) => {
|
||||
const chatMessage = await this.createChatMessage("", messageObject);
|
||||
const isSent = await this.sendMessage(chatMessage);
|
||||
if (isSent == true) this.objectOutbox.remove(messageObject.id);
|
||||
});
|
||||
};
|
||||
|
||||
// messages
|
||||
sendNewMessage = async (): Promise<void> => {
|
||||
if (this.cannotSendMessage.value == true) return;
|
||||
|
||||
await this.sendMessageText(this.composingMessage.value);
|
||||
this.composingMessage.value = "";
|
||||
};
|
||||
|
||||
sendMessageText = async (text: string): Promise<void> => {
|
||||
const message = await this.createMessage(text);
|
||||
this.sendExistingMessage(message);
|
||||
};
|
||||
|
||||
resendMessage = (message: ChatMessage) => {
|
||||
if (this.cannotResendMessage.value == true) return;
|
||||
this.sendMessageText(message.body);
|
||||
};
|
||||
|
||||
createMessage = async (messageText: string): Promise<ChatMessage> => {
|
||||
createChatMessage = async (
|
||||
messageText: string,
|
||||
messageObject?: MessageObject
|
||||
): Promise<ChatMessage> => {
|
||||
// get channels
|
||||
const secondaryChannelNames: string[] = [
|
||||
...this.secondaryChannels.value.values(),
|
||||
@@ -185,54 +225,167 @@ export class Chat {
|
||||
|
||||
// create object
|
||||
const joinedChannelName = allChannelNames.join("/");
|
||||
return {
|
||||
const chatMessage: ChatMessage = {
|
||||
channel: joinedChannelName,
|
||||
sender: senderName.value,
|
||||
body: messageText,
|
||||
isoDate: new Date().toISOString(),
|
||||
};
|
||||
|
||||
if (messageObject)
|
||||
chatMessage.messageObjectString = await encryptString(
|
||||
JSON.stringify(messageObject),
|
||||
this.encryptionKey.value
|
||||
);
|
||||
return chatMessage;
|
||||
};
|
||||
|
||||
sendExistingMessage = async (chatMessage: ChatMessage): Promise<void> => {
|
||||
if (isConnected.value == true && this.isSubscribed.value == true) {
|
||||
const encrypted =
|
||||
this.encryptionKey.value == ""
|
||||
? chatMessage.body
|
||||
: await encryptString(chatMessage.body, this.encryptionKey.value);
|
||||
sendMessageFromComposer = async (): Promise<void> => {
|
||||
if (this.cannotSendMessage.value == true) return;
|
||||
|
||||
chatMessage.body = encrypted;
|
||||
|
||||
const messageString = JSON.stringify(chatMessage);
|
||||
UDN.sendMessage(chatMessage.channel, messageString);
|
||||
} else {
|
||||
this.outbox.add(chatMessage);
|
||||
}
|
||||
await this.sendMessageFromText(this.composingMessage.value);
|
||||
this.composingMessage.value = "";
|
||||
};
|
||||
|
||||
sendMessagesInOutbox = () => {
|
||||
this.outbox.value.forEach((message) => {
|
||||
this.sendExistingMessage(message);
|
||||
this.outbox.remove(message);
|
||||
});
|
||||
sendMessageFromText = async (text: string): Promise<void> => {
|
||||
const chatMessage = await this.createChatMessage(text);
|
||||
this.outbox.add(chatMessage);
|
||||
this.sendMessagesInOutbox();
|
||||
};
|
||||
|
||||
resendMessage = (chatMessage: ChatMessage) => {
|
||||
if (this.cannotResendMessage.value == true) return;
|
||||
this.sendMessageFromText(chatMessage.body);
|
||||
};
|
||||
|
||||
clearMessages = (): void => {
|
||||
this.messages.clear();
|
||||
};
|
||||
|
||||
deleteMessage = (message: ChatMessage): void => {
|
||||
this.messages.remove(message);
|
||||
deleteMessage = (chatMessage: ChatMessage): void => {
|
||||
this.messages.remove(chatMessage);
|
||||
};
|
||||
|
||||
deleteOutboxMessage = (message: ChatMessage): void => {
|
||||
this.outbox.remove(message);
|
||||
deleteOutboxMessage = (chatMessage: ChatMessage): void => {
|
||||
this.outbox.remove(chatMessage);
|
||||
};
|
||||
|
||||
decryptReceivedMessage = async (message: ChatMessage): Promise<void> => {
|
||||
message.body = await decryptString(message.body, this.encryptionKey.value);
|
||||
decryptReceivedMessage = async (chatMessage: ChatMessage): Promise<void> => {
|
||||
chatMessage.body = await decryptString(
|
||||
chatMessage.body,
|
||||
this.encryptionKey.value
|
||||
);
|
||||
this.messages.callSubscriptions();
|
||||
};
|
||||
|
||||
sendMessage = async (chatMessage: ChatMessage): Promise<boolean> => {
|
||||
if (isConnected.value == false || this.isSubscribed.value == false)
|
||||
return false;
|
||||
|
||||
const encryptedBody =
|
||||
this.encryptionKey.value == ""
|
||||
? chatMessage.body
|
||||
: await encryptString(chatMessage.body, this.encryptionKey.value);
|
||||
|
||||
chatMessage.body = encryptedBody;
|
||||
|
||||
const messageString = JSON.stringify(chatMessage);
|
||||
UDN.sendMessage(chatMessage.channel, messageString);
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
// objects
|
||||
createObjectFromTitle = (title: string): MessageObject => {
|
||||
const firstObjectContent = this.createObjectContent();
|
||||
const messageObject: MessageObject = {
|
||||
id: React.UUID(),
|
||||
title,
|
||||
contentVersions: {},
|
||||
};
|
||||
this.addObjectContent(messageObject, firstObjectContent);
|
||||
return messageObject;
|
||||
};
|
||||
|
||||
createObjectContent = () => {
|
||||
return {
|
||||
id: React.UUID(),
|
||||
isoDateVersionCreated: new Date().toISOString(),
|
||||
};
|
||||
};
|
||||
|
||||
addObjectContent = (
|
||||
messageObject: MessageObject,
|
||||
content: MessageObjectContent
|
||||
): void => {
|
||||
messageObject.contentVersions[content.id] = content;
|
||||
};
|
||||
|
||||
addObject = (messageObject: MessageObject): void => {
|
||||
const existingObject = this.objects.value.get(messageObject.id);
|
||||
if (existingObject) {
|
||||
existingObject.title = messageObject.title;
|
||||
Object.values(messageObject.contentVersions).forEach((content) => {
|
||||
this.addObjectContent(existingObject, content);
|
||||
});
|
||||
this.objects.set(existingObject.id, existingObject);
|
||||
} else {
|
||||
this.objects.set(messageObject.id, messageObject);
|
||||
}
|
||||
};
|
||||
|
||||
addObjectAndSend = (messageObject: MessageObject): void => {
|
||||
this.addObject(messageObject);
|
||||
this.sendObject(messageObject);
|
||||
};
|
||||
|
||||
sendObject = (messageObject: MessageObject): void => {
|
||||
this.objectOutbox.set(messageObject.id, messageObject);
|
||||
this.sendMessagesInOutbox();
|
||||
};
|
||||
|
||||
deleteObject = (messageObject: MessageObject): void => {
|
||||
this.objects.remove(messageObject.id);
|
||||
this.objectOutbox.remove(messageObject.id);
|
||||
};
|
||||
|
||||
resendObjects = (): void => {
|
||||
this.objects.value.forEach((messageObject) => {
|
||||
this.sendObject(messageObject);
|
||||
});
|
||||
};
|
||||
|
||||
clearObjects = (): void => {
|
||||
this.objects.clear();
|
||||
};
|
||||
|
||||
getSortedContents = (
|
||||
messageObject: MessageObject
|
||||
): MessageObjectContent[] => {
|
||||
const contents = Object.values(messageObject.contentVersions);
|
||||
contents.sort((a, b) =>
|
||||
a.isoDateVersionCreated > b.isoDateVersionCreated ? 0 : 1
|
||||
);
|
||||
return contents;
|
||||
};
|
||||
|
||||
getMostRecentContentId = (messageObject: MessageObject): string => {
|
||||
const contents = Object.values(messageObject.contentVersions);
|
||||
return contents[contents.length - 1].id;
|
||||
};
|
||||
|
||||
getMostRecentContent = (messageObject: MessageObject): MessageObjectContent => {
|
||||
const id = this.getMostRecentContentId(messageObject);
|
||||
return this.getObjectContentFromId(messageObject, id);
|
||||
}
|
||||
|
||||
getObjectContentFromId = (
|
||||
messageObject: MessageObject,
|
||||
contentId: string
|
||||
): MessageObjectContent => {
|
||||
return messageObject.contentVersions[contentId];
|
||||
};
|
||||
|
||||
// channel
|
||||
setChannel = (): void => {
|
||||
if (this.cannotSetChannel.value == true) return;
|
||||
|
||||
@@ -139,6 +139,7 @@ export const chats = new React.ListState<Chat>();
|
||||
export const chatIds = React.restoreListState<string>("chat-ids");
|
||||
|
||||
export const selectedChat = new React.State<Chat | undefined>(undefined);
|
||||
export const isShowingChatTools = new React.State(false);
|
||||
export const newChatName = new React.State("");
|
||||
|
||||
export const cannotCreateChat = React.createProxyState(
|
||||
@@ -154,6 +155,7 @@ export function createChat() {
|
||||
}
|
||||
|
||||
export function closeChatView() {
|
||||
isShowingChatTools.value = false;
|
||||
selectedChat.value = undefined;
|
||||
document.getElementById("settings-tab")?.scrollIntoView();
|
||||
}
|
||||
@@ -164,6 +166,10 @@ export function selectChat(chat: Chat) {
|
||||
document.getElementById("message-tab")?.scrollIntoView();
|
||||
}
|
||||
|
||||
export function toggleChatTools() {
|
||||
isShowingChatTools.value = !isShowingChatTools.value;
|
||||
}
|
||||
|
||||
chatIds.value.forEach((id) => chats.add(new Chat(id)));
|
||||
|
||||
// INIT
|
||||
|
||||
+20
-5
@@ -1,10 +1,11 @@
|
||||
import * as React from "bloatless-react";
|
||||
|
||||
import { closeChatView, selectedChat } from "../Model/model";
|
||||
import { closeChatView, isShowingChatTools, selectedChat, toggleChatTools } from "../Model/model";
|
||||
|
||||
import { ChatOptionModal } from "../Views/chatOptionModal";
|
||||
import { MessageComposer } from "../Views/messageComposer";
|
||||
import { ThreadView } from "../Views/threadView";
|
||||
import { ChatObjectView } from "../Views/Objects/chatObjectView";
|
||||
import { ChatOptionModal } from "../Views/Chat/chatOptionModal";
|
||||
import { MessageComposer } from "../Views/Chat/messageComposer";
|
||||
import { ThreadView } from "../Views/Chat/threadView";
|
||||
import { translation } from "../translations";
|
||||
|
||||
export function MessageTab() {
|
||||
@@ -33,6 +34,13 @@ export function MessageTab() {
|
||||
<span subscribe:innerText={chat.primaryChannel}></span>
|
||||
</span>
|
||||
<span>
|
||||
<button
|
||||
aria-label={translation.showObjects}
|
||||
on:click={toggleChatTools}
|
||||
toggle:selected={isShowingChatTools}
|
||||
>
|
||||
<span class="icon">deployed_code</span>
|
||||
</button>
|
||||
<button
|
||||
aria-label={translation.showChatOptions}
|
||||
on:click={showOptions}
|
||||
@@ -42,6 +50,7 @@ export function MessageTab() {
|
||||
</span>
|
||||
</header>,
|
||||
|
||||
ChatObjectView(chat),
|
||||
ThreadView(chat),
|
||||
|
||||
<footer>{MessageComposer(chat)}</footer>,
|
||||
@@ -50,5 +59,11 @@ export function MessageTab() {
|
||||
];
|
||||
});
|
||||
|
||||
return <article id="message-tab" children:set={messageTabContent}></article>;
|
||||
return (
|
||||
<article
|
||||
toggle:showingchattools={isShowingChatTools}
|
||||
id="message-tab"
|
||||
children:set={messageTabContent}
|
||||
></article>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,9 +2,9 @@ import * as React from "bloatless-react";
|
||||
|
||||
import { isConnected, isEncryptionAvailable } from "../Model/model";
|
||||
|
||||
import { ChatListSection } from "../Views/chatListSection";
|
||||
import { ConnectionSection } from "../Views/connectionSection";
|
||||
import { PersonalSection } from "../Views/personalSection";
|
||||
import { ChatListSection } from "../Views/Overview/chatListSection";
|
||||
import { ConnectionSection } from "../Views/Overview/connectionSection";
|
||||
import { PersonalSection } from "../Views/Overview/personalSection";
|
||||
import { translation } from "../translations";
|
||||
|
||||
export function OverviewTab() {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import * as React from "bloatless-react";
|
||||
|
||||
import { Chat } from "../Model/chatModel";
|
||||
import { closeChatView } from "../Model/model";
|
||||
import { translation } from "../translations";
|
||||
import { Chat } from "../../Model/chatModel";
|
||||
import { closeChatView } from "../../Model/model";
|
||||
import { translation } from "../../translations";
|
||||
|
||||
export function ChatOptionModal(chat: Chat, isPresented: React.State<boolean>) {
|
||||
function closeModal() {
|
||||
@@ -20,7 +20,7 @@ export function ChatOptionModal(chat: Chat, isPresented: React.State<boolean>) {
|
||||
shouldShowKey.value == true ? "text" : "password"
|
||||
);
|
||||
|
||||
const secondaryChannelConverter: React.ListItemConverter<string> = (
|
||||
const secondaryChannelConverter: React.StateItemConverter<string> = (
|
||||
secondaryChannel
|
||||
) => {
|
||||
function remove() {
|
||||
@@ -131,19 +131,37 @@ export function ChatOptionModal(chat: Chat, isPresented: React.State<boolean>) {
|
||||
|
||||
<hr></hr>
|
||||
|
||||
<div class="flex-column gap">
|
||||
<div class="flex-column gap width-input">
|
||||
<button on:click={chat.resendObjects}>
|
||||
{translation.resendObjects}
|
||||
<span class="icon">replay</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<hr></hr>
|
||||
|
||||
<div class="flex-column gap width-input">
|
||||
<button
|
||||
class="danger"
|
||||
on:click={chat.clearObjects}
|
||||
toggle:disabled={chat.cannotClearObjects}
|
||||
>
|
||||
{translation.clearObjects}
|
||||
<span class="icon">deployed_code</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="danger"
|
||||
on:click={chat.clearMessages}
|
||||
toggle:disabled={chat.cannotClearMessages}
|
||||
>
|
||||
{translation.clearChatMessages}
|
||||
<span class="icon">delete_sweep</span>
|
||||
<span class="icon">chat_error</span>
|
||||
</button>
|
||||
|
||||
<button class="danger" on:click={deleteChat}>
|
||||
{translation.removeChat}
|
||||
<span class="icon">delete</span>
|
||||
<span class="icon">delete_forever</span>
|
||||
</button>
|
||||
</div>
|
||||
</main>
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as React from "bloatless-react";
|
||||
|
||||
import { Chat } from "../Model/chatModel";
|
||||
import { translation } from "../translations";
|
||||
import { Chat } from "../../Model/chatModel";
|
||||
import { translation } from "../../translations";
|
||||
|
||||
export function MessageComposer(chat: Chat) {
|
||||
return (
|
||||
@@ -12,11 +12,11 @@ export function MessageComposer(chat: Chat) {
|
||||
style="max-width: unset"
|
||||
placeholder={translation.composerPlaceholder}
|
||||
bind:value={chat.composingMessage}
|
||||
on:enter={chat.sendNewMessage}
|
||||
on:enter={chat.sendMessageFromComposer}
|
||||
></input>
|
||||
<button
|
||||
class="primary"
|
||||
on:click={chat.sendNewMessage}
|
||||
on:click={chat.sendMessageFromComposer}
|
||||
toggle:disabled={chat.cannotSendMessage}
|
||||
>
|
||||
<span class="icon">send</span>
|
||||
@@ -1,11 +1,12 @@
|
||||
import * as React from "bloatless-react";
|
||||
|
||||
import { Chat, ChatMessage } from "../Model/chatModel";
|
||||
import { Chat, ChatMessage } from "../../Model/chatModel";
|
||||
|
||||
import { translation } from "../translations";
|
||||
import { isShowingChatTools } from "../../Model/model";
|
||||
import { translation } from "../../translations";
|
||||
|
||||
export function ThreadView(chat: Chat) {
|
||||
const messageConverter: React.ListItemConverter<ChatMessage> = (message) => {
|
||||
const messageConverter: React.StateItemConverter<ChatMessage> = (message) => {
|
||||
function resendMessage() {
|
||||
chat.resendMessage(message);
|
||||
}
|
||||
@@ -72,7 +73,7 @@ export function ThreadView(chat: Chat) {
|
||||
);
|
||||
};
|
||||
|
||||
const outboxMessageConverter: React.ListItemConverter<ChatMessage> = (
|
||||
const outboxMessageConverter: React.StateItemConverter<ChatMessage> = (
|
||||
message
|
||||
) => {
|
||||
function remove() {
|
||||
@@ -116,24 +117,29 @@ export function ThreadView(chat: Chat) {
|
||||
></div>
|
||||
);
|
||||
const listWrapper = (
|
||||
<div class="flex-column gap">
|
||||
<div class="thread-view flex-column gap">
|
||||
{messageList}
|
||||
{outboxList}
|
||||
</div>
|
||||
);
|
||||
|
||||
function scrollToBottom() {
|
||||
listWrapper.scrollTop = listWrapper.scrollHeight;
|
||||
}
|
||||
|
||||
function scrollToBottomIfAppropriate() {
|
||||
const scrollFromBottom =
|
||||
listWrapper.scrollHeight -
|
||||
(listWrapper.scrollTop + listWrapper.offsetHeight);
|
||||
if (scrollFromBottom > 400) return;
|
||||
|
||||
listWrapper.scrollTop = listWrapper.scrollHeight;
|
||||
scrollToBottom();
|
||||
}
|
||||
|
||||
chat.messages.handleAddition(scrollToBottom);
|
||||
chat.outbox.handleAddition(scrollToBottom);
|
||||
setTimeout(() => (listWrapper.scrollTop = listWrapper.scrollHeight), 50);
|
||||
chat.messages.handleAddition(scrollToBottomIfAppropriate);
|
||||
chat.outbox.handleAddition(scrollToBottomIfAppropriate);
|
||||
isShowingChatTools.subscribe(() => scrollToBottom());
|
||||
setTimeout(() => scrollToBottom(), 50);
|
||||
|
||||
return listWrapper;
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import * as React from "bloatless-react";
|
||||
|
||||
import { Chat, MessageObject } from "../../Model/chatModel";
|
||||
|
||||
import { translation } from "../../translations";
|
||||
|
||||
export function AllObjectsView(
|
||||
chat: Chat,
|
||||
selectedObject: React.State<MessageObject | undefined>,
|
||||
isShowingObjectModal: React.State<boolean>
|
||||
) {
|
||||
const objectConverter: React.StateItemConverter<MessageObject> = (
|
||||
messageObject
|
||||
) => {
|
||||
function select() {
|
||||
selectedObject.value = messageObject;
|
||||
isShowingObjectModal.value = true;
|
||||
}
|
||||
|
||||
return (
|
||||
<button class="tile" on:click={select}>
|
||||
<div>
|
||||
<b>{messageObject.title}</b>
|
||||
<span class="secondary">{messageObject.id}</span>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
const content = React.createProxyState([chat.objects], () =>
|
||||
chat.objects.value.size == 0 ? (
|
||||
<div class="flex-column width-100 height-100 align-center justify-center secondary">
|
||||
{translation.noObjects}
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
class="grid gap padding"
|
||||
style="grid-template-columns: repeat(auto-fill, minmax(350px, 1fr))"
|
||||
children:prepend={[chat.objects, objectConverter]}
|
||||
></div>
|
||||
)
|
||||
);
|
||||
|
||||
return <div class="width-100 height-100" children:set={content}></div>;
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
import * as React from "bloatless-react";
|
||||
|
||||
import { Chat, MessageObject } from "../../Model/chatModel";
|
||||
|
||||
import { AllObjectsView } from "./allObjectsView";
|
||||
import { NoteObjectsView } from "./noteObjectsView";
|
||||
import { ObjectDetailModal } from "./objectDetailModal";
|
||||
import { translation } from "../../translations";
|
||||
|
||||
export const viewTypes = {
|
||||
all: [translation.viewAll, "grid_view"],
|
||||
notes: [translation.viewNotes, "sticky_note_2"],
|
||||
};
|
||||
|
||||
export function ChatObjectView(chat: Chat) {
|
||||
// selection and modal
|
||||
const isShowingObjectModal = new React.State(false);
|
||||
const selectedObject = new React.State<MessageObject | undefined>(undefined);
|
||||
const objectModal = React.createProxyState(
|
||||
[chat.objects, selectedObject],
|
||||
() => {
|
||||
if (selectedObject.value == undefined) return <div></div>;
|
||||
|
||||
return ObjectDetailModal(
|
||||
chat,
|
||||
selectedObject.value,
|
||||
isShowingObjectModal
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
// view type
|
||||
const selectedViewType = new React.State<keyof typeof viewTypes>("all");
|
||||
const mainView = React.createProxyState([selectedViewType], () => {
|
||||
function getViewFunction() {
|
||||
switch (selectedViewType.value) {
|
||||
case "notes":
|
||||
return NoteObjectsView;
|
||||
default:
|
||||
return AllObjectsView;
|
||||
}
|
||||
}
|
||||
|
||||
return getViewFunction()(chat, selectedObject, isShowingObjectModal);
|
||||
});
|
||||
|
||||
// methods
|
||||
function createObject() {
|
||||
const newObject = chat.createObjectFromTitle(translation.untitledObject);
|
||||
chat.addObjectAndSend(newObject);
|
||||
selectedObject.value = newObject;
|
||||
isShowingObjectModal.value = true;
|
||||
}
|
||||
|
||||
return (
|
||||
<div class="chat-object-view flex-column">
|
||||
<div class="flex-row align-center border-bottom">
|
||||
<button
|
||||
class="primary height-100"
|
||||
on:click={createObject}
|
||||
aria-label={translation.createObject}
|
||||
>
|
||||
<span class="icon">add</span>
|
||||
</button>
|
||||
|
||||
<div class="padding-sm flex flex-row gap justify-center scroll-h width-100">
|
||||
{...Object.keys(viewTypes).map((key: any) =>
|
||||
ViewTypeToggle(key, selectedViewType)
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="height-100"
|
||||
on:click={createObject}
|
||||
aria-label={translation.createObject}
|
||||
>
|
||||
<span class="icon">visibility</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="width-100 height-100 flex-1 scroll-h"
|
||||
children:set={mainView}
|
||||
></div>
|
||||
|
||||
<div children:set={objectModal}></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ViewTypeToggle(
|
||||
key: keyof typeof viewTypes,
|
||||
selection: React.State<keyof typeof viewTypes>
|
||||
) {
|
||||
const [label, icon] = viewTypes[key];
|
||||
|
||||
function select() {
|
||||
selection.value = key;
|
||||
}
|
||||
|
||||
const isSelected = React.createProxyState(
|
||||
[selection],
|
||||
() => selection.value == key
|
||||
);
|
||||
|
||||
return (
|
||||
<button aria-label={label} on:click={select} toggle:selected={isSelected}>
|
||||
<span class="icon">{icon}</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import * as React from "bloatless-react";
|
||||
|
||||
import { Chat, MessageObject } from "../../Model/chatModel";
|
||||
|
||||
import { translation } from "../../translations";
|
||||
|
||||
export function NoteObjectsView(
|
||||
chat: Chat,
|
||||
selectedObject: React.State<MessageObject | undefined>,
|
||||
isShowingObjectModal: React.State<boolean>
|
||||
) {
|
||||
const objectConverter: React.StateItemConverter<MessageObject> = (
|
||||
messageObject
|
||||
) => {
|
||||
const latest = chat.getMostRecentContent(messageObject);
|
||||
|
||||
function select() {
|
||||
selectedObject.value = messageObject;
|
||||
isShowingObjectModal.value = true;
|
||||
}
|
||||
|
||||
return (
|
||||
<button class="tile" on:click={select}>
|
||||
<div>
|
||||
<b>{messageObject.title}</b>
|
||||
<span class="secondary ellipsis">
|
||||
{latest.noteContent!.split("\n")[0]}
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
const notes = new React.ListState<MessageObject>();
|
||||
chat.objects.subscribe(() => {
|
||||
notes.clear();
|
||||
chat.objects.value.forEach((messageObject) => {
|
||||
const latest = chat.getMostRecentContent(messageObject);
|
||||
if (!latest.noteContent) return;
|
||||
|
||||
notes.add(messageObject);
|
||||
});
|
||||
});
|
||||
|
||||
const content = React.createProxyState([chat.objects], () =>
|
||||
notes.value.size == 0 ? (
|
||||
<div class="flex-column width-100 height-100 align-center justify-center secondary">
|
||||
{translation.noNotes}
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
class="grid gap padding"
|
||||
style="grid-template-columns: repeat(auto-fill, minmax(350px, 1fr))"
|
||||
children:prepend={[notes, objectConverter]}
|
||||
></div>
|
||||
)
|
||||
);
|
||||
|
||||
return <div class="width-100 height-100" children:set={content}></div>;
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
import * as React from "bloatless-react";
|
||||
|
||||
import { Chat, MessageObject } from "../../Model/chatModel";
|
||||
|
||||
import { translation } from "../../translations";
|
||||
|
||||
export function ObjectDetailModal(
|
||||
chat: Chat,
|
||||
messageObject: MessageObject,
|
||||
isPresented: React.State<boolean>
|
||||
) {
|
||||
// state
|
||||
const editingTitle = new React.State(messageObject.title);
|
||||
|
||||
const selectedMessageObjectId = new React.State(
|
||||
chat.getMostRecentContentId(messageObject)
|
||||
);
|
||||
const selectedMessageObject = React.createProxyState(
|
||||
[selectedMessageObjectId],
|
||||
() =>
|
||||
chat.getObjectContentFromId(messageObject, selectedMessageObjectId.value)
|
||||
);
|
||||
|
||||
const didEditContent = new React.State(false);
|
||||
const editingNoteContent = React.createProxyState(
|
||||
[selectedMessageObject],
|
||||
() => selectedMessageObject.value.noteContent ?? ""
|
||||
);
|
||||
React.bulkSubscribe(
|
||||
[editingNoteContent],
|
||||
() => (didEditContent.value = true)
|
||||
);
|
||||
|
||||
// methods
|
||||
function handleKeyDown(e: KeyboardEvent) {
|
||||
if (!e.metaKey && !e.ctrlKey) return;
|
||||
|
||||
switch (e.key) {
|
||||
case "s":
|
||||
e.preventDefault();
|
||||
saveAndClose();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
isPresented.value = false;
|
||||
}
|
||||
|
||||
function saveAndClose() {
|
||||
messageObject.title = editingTitle.value;
|
||||
|
||||
if (didEditContent.value == true) {
|
||||
chat.addObjectContent(messageObject, {
|
||||
isoDateVersionCreated: new Date().toISOString(),
|
||||
id: React.UUID(),
|
||||
|
||||
noteContent: editingNoteContent.value,
|
||||
});
|
||||
}
|
||||
|
||||
chat.addObjectAndSend(messageObject);
|
||||
closeModal();
|
||||
}
|
||||
|
||||
function deleteAndClose() {
|
||||
chat.deleteObject(messageObject);
|
||||
closeModal();
|
||||
}
|
||||
|
||||
return (
|
||||
<div class="modal" toggle:open={isPresented} on:keydown={handleKeyDown}>
|
||||
<div>
|
||||
<main>
|
||||
<h2>{messageObject.title}</h2>
|
||||
<span class="secondary">{messageObject.id}</span>
|
||||
|
||||
<hr></hr>
|
||||
|
||||
<div class="flex-column gap">
|
||||
<label class="tile">
|
||||
<span class="icon">label</span>
|
||||
<div>
|
||||
<span>{translation.objectTitle}</span>
|
||||
<input
|
||||
autofocus
|
||||
bind:value={editingTitle}
|
||||
placeholder={translation.objectTitlePlaceholder}
|
||||
></input>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<hr></hr>
|
||||
|
||||
<label class="tile">
|
||||
<span class="icon">history</span>
|
||||
<div>
|
||||
<span>{translation.objectVersion}</span>
|
||||
<select bind:value={selectedMessageObjectId}>
|
||||
{...chat
|
||||
.getSortedContents(messageObject)
|
||||
.map((content) => (
|
||||
<option value={content.id}>
|
||||
{new Date(
|
||||
content.isoDateVersionCreated
|
||||
).toLocaleString()}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<span class="icon">arrow_drop_down</span>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<label class="tile">
|
||||
<span class="icon">sticky_note_2</span>
|
||||
<div>
|
||||
<span>{translation.noteContent}</span>
|
||||
<textarea
|
||||
rows="5"
|
||||
bind:value={editingNoteContent}
|
||||
placeholder={translation.noteContentPlaceholder}
|
||||
></textarea>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<hr></hr>
|
||||
|
||||
<button class="danger width-input" on:click={deleteAndClose}>
|
||||
{translation.deleteObject}
|
||||
<span class="icon">delete</span>
|
||||
</button>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<div class="flex-row">
|
||||
<button class="flex-1 width-100 danger" on:click={closeModal}>
|
||||
{translation.discard}
|
||||
</button>
|
||||
<button class="flex-1 width-100 primary" on:click={saveAndClose}>
|
||||
{translation.save}
|
||||
<span class="icon">save</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -7,12 +7,12 @@ import {
|
||||
newChatName,
|
||||
selectChat,
|
||||
selectedChat,
|
||||
} from "../Model/model";
|
||||
} from "../../Model/model";
|
||||
|
||||
import { Chat } from "../Model/chatModel";
|
||||
import { translation } from "../translations";
|
||||
import { Chat } from "../../Model/chatModel";
|
||||
import { translation } from "../../translations";
|
||||
|
||||
const chatConverter: React.ListItemConverter<Chat> = (chat) => {
|
||||
const chatConverter: React.StateItemConverter<Chat> = (chat) => {
|
||||
function select() {
|
||||
selectChat(chat);
|
||||
}
|
||||
@@ -13,11 +13,11 @@ import {
|
||||
requestMailbox,
|
||||
resetAddressInput,
|
||||
serverAddressInput,
|
||||
} from "../Model/model";
|
||||
} from "../../Model/model";
|
||||
|
||||
import { translation } from "../translations";
|
||||
import { translation } from "../../translations";
|
||||
|
||||
const addressConverter: React.ListItemConverter<string> = (address) => {
|
||||
const addressConverter: React.StateItemConverter<string> = (address) => {
|
||||
return <option>{address}</option>;
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as React from "bloatless-react";
|
||||
|
||||
import { senderName } from "../Model/model";
|
||||
import { translation } from "../translations";
|
||||
import { senderName } from "../../Model/model";
|
||||
import { translation } from "../../translations";
|
||||
|
||||
export function PersonalSection() {
|
||||
return (
|
||||
@@ -1,3 +1,89 @@
|
||||
[unread] {
|
||||
border-left: 10px solid var(--text-warn);
|
||||
}
|
||||
|
||||
/* TOOLS */
|
||||
:root {
|
||||
--thread-view-height-with-tools: 8rem;
|
||||
--thread-view-width-with-tools: 20rem;
|
||||
--footer-top-line: calc(var(--control-height) + var(--safe-area-bottom));
|
||||
}
|
||||
|
||||
#message-tab {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.message-tab-content-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.chat-object-view {
|
||||
display: none;
|
||||
}
|
||||
|
||||
[showingchattools] :is(.thread-view, .chat-object-view) {
|
||||
overflow: scroll;
|
||||
position: absolute;
|
||||
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
[showingchattools] header {
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
[showingchattools] .chat-object-view {
|
||||
display: flex;
|
||||
|
||||
top: var(--header-height);
|
||||
bottom: calc(var(--thread-view-height-with-tools) + var(--footer-top-line));
|
||||
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
[showingchattools] .thread-view {
|
||||
display: flex;
|
||||
|
||||
height: var(--thread-view-height-with-tools);
|
||||
|
||||
top: unset;
|
||||
bottom: var(--footer-top-line);
|
||||
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) and (max-width: 1400px) {
|
||||
main.split:has([showingchattools]) {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
main.split:has([showingchattools]) #settings-tab {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
[showingchattools] :is(.thread-view, .chat-object-view) {
|
||||
top: var(--header-height);
|
||||
bottom: var(--footer-top-line);
|
||||
}
|
||||
|
||||
[showingchattools] .chat-object-view {
|
||||
left: 0px;
|
||||
right: var(--thread-view-width-with-tools);
|
||||
|
||||
border-bottom: none;
|
||||
border-right: 1px solid var(--border);
|
||||
}
|
||||
|
||||
[showingchattools] .thread-view {
|
||||
display: flex;
|
||||
|
||||
left: unset;
|
||||
right: 0px;
|
||||
|
||||
height: unset;
|
||||
width: var(--thread-view-width-with-tools);
|
||||
}
|
||||
}
|
||||
|
||||
+93
-18
@@ -1,9 +1,11 @@
|
||||
const englishTranslations = {
|
||||
// general
|
||||
set: "Set",
|
||||
save: "Save",
|
||||
back: "Back",
|
||||
undoChanges: "Undo changes",
|
||||
close: "Close",
|
||||
discard: "Discard",
|
||||
|
||||
// overview
|
||||
overview: "Overview",
|
||||
@@ -22,7 +24,7 @@ const englishTranslations = {
|
||||
serverAddressPlaceholder: "wss://192.168.0.69:3000",
|
||||
|
||||
connectToServer: "Connect",
|
||||
disconnect: "Disonnect",
|
||||
disconnect: "Disconnect",
|
||||
|
||||
mailbox: "Mailbox",
|
||||
requestMailbox: "Enable",
|
||||
@@ -34,7 +36,7 @@ const englishTranslations = {
|
||||
primaryChannelPlaceholder: "my-channel",
|
||||
addChat: "Add",
|
||||
|
||||
// messages
|
||||
// chat
|
||||
showChatOptions: "show chat options",
|
||||
configureChatTitle: "Configure Chat",
|
||||
|
||||
@@ -47,8 +49,9 @@ const englishTranslations = {
|
||||
encryptionKeyPlaceholder: "n10d2482dg283hg",
|
||||
showKey: "Show key",
|
||||
|
||||
removeChat: "Remove chat",
|
||||
clearObjects: "Delete all objects",
|
||||
clearChatMessages: "Clear chat messages",
|
||||
removeChat: "Remove chat",
|
||||
|
||||
messageInOutbox: "Pending",
|
||||
|
||||
@@ -57,10 +60,32 @@ const englishTranslations = {
|
||||
composerPlaceholder: "Type a message...",
|
||||
sendMessage: "Send",
|
||||
|
||||
// messages
|
||||
resendMessage: "Resend message",
|
||||
decryptMessage: "Decrypt message",
|
||||
copyMessage: "Copy message",
|
||||
deleteMessage: "Delete message",
|
||||
|
||||
// objects
|
||||
showObjects: "show objects",
|
||||
createObject: "create object",
|
||||
untitledObject: "Untitled Object",
|
||||
|
||||
viewAll: "All",
|
||||
viewNotes: "Notes",
|
||||
|
||||
noObjects: "No objects",
|
||||
noNotes: "No notes",
|
||||
|
||||
objectTitle: "Object title",
|
||||
objectTitlePlaceholder: "My object",
|
||||
|
||||
objectVersion: "Object version",
|
||||
noteContent: "Note",
|
||||
noteContentPlaceholder: "Take a note...",
|
||||
|
||||
resendObjects: "Resend all objects",
|
||||
deleteObject: "Delete object",
|
||||
};
|
||||
|
||||
const allTranslations: { [key: string]: typeof englishTranslations } = {
|
||||
@@ -68,14 +93,16 @@ const allTranslations: { [key: string]: typeof englishTranslations } = {
|
||||
es: {
|
||||
// general
|
||||
set: "Guardar",
|
||||
save: "Guardar",
|
||||
back: "Atrás",
|
||||
undoChanges: "Deshacer",
|
||||
close: "Cerrar",
|
||||
discard: "Descartar",
|
||||
|
||||
// overview
|
||||
overview: "Resumen",
|
||||
|
||||
connection: "Conexion",
|
||||
connection: "Conexión",
|
||||
chats: "Chats",
|
||||
|
||||
yourName: "Tu nombre",
|
||||
@@ -85,7 +112,7 @@ const allTranslations: { [key: string]: typeof englishTranslations } = {
|
||||
encryptionUnavailableMessage:
|
||||
"Obtén esta aplicación a traves de HTTPS o continúa sin cifrado",
|
||||
|
||||
serverAddress: "Direccion del servidor",
|
||||
serverAddress: "Dirección del servidor",
|
||||
serverAddressPlaceholder: "wss://192.168.0.69:3000",
|
||||
|
||||
connectToServer: "Conectar",
|
||||
@@ -101,40 +128,65 @@ const allTranslations: { [key: string]: typeof englishTranslations } = {
|
||||
primaryChannelPlaceholder: "mi-canal",
|
||||
addChat: "Añadir",
|
||||
|
||||
// messages
|
||||
// chat
|
||||
showChatOptions: "Mostrar opciones del chat",
|
||||
configureChatTitle: "Configurar chat",
|
||||
|
||||
secondaryChannel: "Canal segundario",
|
||||
secondaryChannelPlaceholder: "Añadir canal segundario",
|
||||
addSecondaryChannel: "Añadir canal segundario",
|
||||
removeSecondaryChannel: "Eliminar canal segundario",
|
||||
secondaryChannel: "Canal secundario",
|
||||
secondaryChannelPlaceholder: "Añadir canal secundario",
|
||||
addSecondaryChannel: "Añadir canal secundario",
|
||||
removeSecondaryChannel: "Eliminar canal secundario",
|
||||
|
||||
encryptionKey: "Clave de cifrado",
|
||||
encryptionKeyPlaceholder: "n10d2482dg283hg",
|
||||
showKey: "Mostrar clave",
|
||||
|
||||
clearObjects: "Eliminar todos los objetos",
|
||||
clearChatMessages: "Eliminar todos los mensajes",
|
||||
removeChat: "Eliminar chat",
|
||||
clearChatMessages: "Eliminar todos mensajes",
|
||||
|
||||
noChatSelected: "Selecciona un chat",
|
||||
|
||||
messageInOutbox: "Pendiente",
|
||||
|
||||
noChatSelected: "Selecciona un chat",
|
||||
|
||||
composerPlaceholder: "Escribe un mensaje...",
|
||||
sendMessage: "Enviar",
|
||||
|
||||
resendMessage: "Enviar de nuevo",
|
||||
// messages
|
||||
resendMessage: "Reenviar mensaje",
|
||||
decryptMessage: "Descifrar mensaje",
|
||||
copyMessage: "Copiar mensaje",
|
||||
deleteMessage: "Eliminar este mensaje",
|
||||
|
||||
// objects
|
||||
showObjects: "mostrar objetos",
|
||||
createObject: "añadir nuevo objeto",
|
||||
untitledObject: "Sin Título",
|
||||
|
||||
viewAll: "Todos",
|
||||
viewNotes: "Notas",
|
||||
|
||||
noObjects: "Sin objetos",
|
||||
noNotes: "Sin notas",
|
||||
|
||||
objectTitle: "Título",
|
||||
objectTitlePlaceholder: "Mi objeto",
|
||||
|
||||
objectVersion: "Version del objeto",
|
||||
noteContent: "Nota",
|
||||
noteContentPlaceholder: "Toma nota...",
|
||||
|
||||
resendObjects: "Reenviar todos objetos",
|
||||
deleteObject: "Eliminar objeto",
|
||||
},
|
||||
de: {
|
||||
// general
|
||||
set: "OK",
|
||||
save: "Sichern",
|
||||
back: "Zurück",
|
||||
undoChanges: "Änderungen verwerfen",
|
||||
close: "Schließen",
|
||||
discard: "Verwerfen",
|
||||
|
||||
// overview
|
||||
overview: "Übersicht",
|
||||
@@ -165,7 +217,7 @@ const allTranslations: { [key: string]: typeof englishTranslations } = {
|
||||
primaryChannelPlaceholder: "mein-kanal",
|
||||
addChat: "Hinzufügen",
|
||||
|
||||
// messages
|
||||
// chat
|
||||
showChatOptions: "Chatoptionen einblenden",
|
||||
configureChatTitle: "Chat konfigurieren",
|
||||
|
||||
@@ -178,20 +230,43 @@ const allTranslations: { [key: string]: typeof englishTranslations } = {
|
||||
encryptionKeyPlaceholder: "n10d2482dg283hg",
|
||||
showKey: "Schlüssel anzeigen",
|
||||
|
||||
removeChat: "Chat löschen",
|
||||
clearObjects: "Alle Objekte löschen",
|
||||
clearChatMessages: "Nachrichtenverlauf leeren",
|
||||
|
||||
noChatSelected: "Kein Chat ausgewählt",
|
||||
removeChat: "Chat löschen",
|
||||
|
||||
messageInOutbox: "Ausstehend",
|
||||
|
||||
noChatSelected: "Kein Chat ausgewählt",
|
||||
|
||||
composerPlaceholder: "Neue Nachricht...",
|
||||
sendMessage: "Senden",
|
||||
|
||||
// messages
|
||||
resendMessage: "Erneut senden",
|
||||
decryptMessage: "Nachricht entschlüsseln",
|
||||
copyMessage: "Nachricht kopieren",
|
||||
deleteMessage: "Nachricht löschen",
|
||||
|
||||
// objects
|
||||
showObjects: "Objekte anzeigen",
|
||||
createObject: "Neues Objekt erstellen",
|
||||
untitledObject: "Unbenannt",
|
||||
|
||||
viewAll: "Alle",
|
||||
viewNotes: "Notizen",
|
||||
|
||||
noObjects: "Keine Objekte",
|
||||
noNotes: "Keine Notizen",
|
||||
|
||||
objectTitle: "Titel",
|
||||
objectTitlePlaceholder: "Mein Objekt",
|
||||
|
||||
objectVersion: "Version des Objekts",
|
||||
noteContent: "Notiz",
|
||||
noteContentPlaceholder: "Notiz eingeben...",
|
||||
|
||||
resendObjects: "Objekte erneut senden",
|
||||
deleteObject: "Objekt löschen",
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -19,10 +19,18 @@ export const storageKeys = {
|
||||
return id + "messages";
|
||||
},
|
||||
|
||||
objects(id: string): string {
|
||||
return id + "items";
|
||||
},
|
||||
|
||||
outbox(id: string): string {
|
||||
return id + "outbox";
|
||||
},
|
||||
|
||||
itemOutbox(id: string): string {
|
||||
return id + "item-outbox";
|
||||
},
|
||||
|
||||
composingMessage(id: string): string {
|
||||
return id + "composing-message";
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user