delete src
This commit is contained in:
@@ -1,494 +0,0 @@
|
|||||||
import * as React from "bloatless-react";
|
|
||||||
|
|
||||||
import {
|
|
||||||
UDN,
|
|
||||||
chatIds,
|
|
||||||
chats,
|
|
||||||
isConnected,
|
|
||||||
outboxItemCount,
|
|
||||||
selectedChat,
|
|
||||||
senderName,
|
|
||||||
updateMailbox,
|
|
||||||
usedObjectCategories,
|
|
||||||
usedObjectStatuses,
|
|
||||||
} from "../Model/model";
|
|
||||||
import { decryptString, encryptString } from "../cryptUtility";
|
|
||||||
|
|
||||||
import { Message } from "udn-frontend";
|
|
||||||
import { storageKeys } from "../utility";
|
|
||||||
import { translation } from "../translations";
|
|
||||||
import { viewTypes } from "../Views/Objects/objectPane";
|
|
||||||
|
|
||||||
// TYPES
|
|
||||||
// message
|
|
||||||
export interface ChatMessage {
|
|
||||||
channel: string;
|
|
||||||
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;
|
|
||||||
priority?: number;
|
|
||||||
categoryName?: string;
|
|
||||||
status?: string;
|
|
||||||
date?: string;
|
|
||||||
time?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MessageObjectWithIndex extends MessageObject {
|
|
||||||
index: React.State<number>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class MessageObjectWithIndex implements MessageObjectWithIndex {
|
|
||||||
constructor(messageObject: MessageObject, index: number = 0) {
|
|
||||||
this.id = messageObject.id;
|
|
||||||
this.title = messageObject.title;
|
|
||||||
this.contentVersions = messageObject.contentVersions;
|
|
||||||
this.index = new React.State(index);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// chat
|
|
||||||
export class Chat {
|
|
||||||
id: string;
|
|
||||||
|
|
||||||
viewType: React.State<keyof typeof viewTypes>;
|
|
||||||
|
|
||||||
isSubscribed = new React.State(false);
|
|
||||||
primaryChannel = new React.State("");
|
|
||||||
hasUnreadMessages: React.State<boolean>;
|
|
||||||
secondaryChannels: React.ListState<string>;
|
|
||||||
|
|
||||||
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>;
|
|
||||||
primaryChannelInput: React.State<string>;
|
|
||||||
newSecondaryChannelName: React.State<string>;
|
|
||||||
|
|
||||||
cannotSendMessage: React.State<boolean>;
|
|
||||||
cannotResendMessage: React.State<boolean>;
|
|
||||||
cannotAddSecondaryChannel: React.State<boolean>;
|
|
||||||
cannotSetChannel: React.State<boolean>;
|
|
||||||
cannotUndoChannel: React.State<boolean>;
|
|
||||||
cannotClearMessages: React.State<boolean>;
|
|
||||||
cannotClearObjects: React.State<boolean>;
|
|
||||||
|
|
||||||
// init
|
|
||||||
constructor(id: string = React.UUID()) {
|
|
||||||
this.id = id;
|
|
||||||
|
|
||||||
this.viewType = React.restoreState<keyof typeof viewTypes>(
|
|
||||||
storageKeys.viewType(id),
|
|
||||||
"all"
|
|
||||||
);
|
|
||||||
|
|
||||||
// channels
|
|
||||||
this.primaryChannel = React.restoreState(
|
|
||||||
storageKeys.primaryChannel(id),
|
|
||||||
""
|
|
||||||
);
|
|
||||||
this.secondaryChannels = React.restoreListState(
|
|
||||||
storageKeys.secondaryChannels(id)
|
|
||||||
);
|
|
||||||
|
|
||||||
// messaging
|
|
||||||
this.hasUnreadMessages = React.restoreState(
|
|
||||||
storageKeys.hasUnread(id),
|
|
||||||
false
|
|
||||||
);
|
|
||||||
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
|
|
||||||
);
|
|
||||||
|
|
||||||
// outbox
|
|
||||||
React.bulkSubscribe([this.outbox, this.objectOutbox], () =>
|
|
||||||
this.updateOutboxCount()
|
|
||||||
);
|
|
||||||
this.updateOutboxCount();
|
|
||||||
|
|
||||||
// inputs
|
|
||||||
this.composingMessage = React.restoreState(
|
|
||||||
storageKeys.composingMessage(id),
|
|
||||||
""
|
|
||||||
);
|
|
||||||
this.newSecondaryChannelName = new React.State("");
|
|
||||||
this.primaryChannelInput = new React.State(this.primaryChannel.value);
|
|
||||||
|
|
||||||
// guards
|
|
||||||
this.cannotSendMessage = React.createProxyState(
|
|
||||||
[this.primaryChannel, this.composingMessage, senderName],
|
|
||||||
() =>
|
|
||||||
this.primaryChannel.value == "" ||
|
|
||||||
this.composingMessage.value == "" ||
|
|
||||||
senderName.value == ""
|
|
||||||
);
|
|
||||||
this.cannotResendMessage = React.createProxyState(
|
|
||||||
[this.primaryChannel, senderName],
|
|
||||||
() => this.primaryChannel.value == "" || senderName.value == ""
|
|
||||||
);
|
|
||||||
this.cannotAddSecondaryChannel = React.createProxyState(
|
|
||||||
[this.newSecondaryChannelName],
|
|
||||||
() => this.newSecondaryChannelName.value == ""
|
|
||||||
);
|
|
||||||
this.cannotSetChannel = React.createProxyState(
|
|
||||||
[this.primaryChannelInput, this.primaryChannel],
|
|
||||||
() =>
|
|
||||||
this.primaryChannelInput.value == "" ||
|
|
||||||
this.primaryChannelInput.value == this.primaryChannel.value
|
|
||||||
);
|
|
||||||
this.cannotUndoChannel = React.createProxyState(
|
|
||||||
[this.primaryChannelInput, this.primaryChannel],
|
|
||||||
() => this.primaryChannelInput.value == this.primaryChannel.value
|
|
||||||
);
|
|
||||||
this.cannotClearMessages = React.createProxyState(
|
|
||||||
[this.messages],
|
|
||||||
() => this.messages.value.size == 0
|
|
||||||
);
|
|
||||||
this.cannotClearObjects = React.createProxyState(
|
|
||||||
[this.objects],
|
|
||||||
() => this.objects.value.size == 0
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// general
|
|
||||||
deleteSelf = () => {
|
|
||||||
Object.values(storageKeys).forEach((storageKey) => {
|
|
||||||
localStorage.removeItem(storageKey(this.id));
|
|
||||||
});
|
|
||||||
|
|
||||||
chats.remove(this);
|
|
||||||
chatIds.remove(this.id);
|
|
||||||
};
|
|
||||||
|
|
||||||
// handlers
|
|
||||||
onmessage = async (data: Message): Promise<void> => {
|
|
||||||
if (!data.messageChannel) return;
|
|
||||||
const channels = data.messageChannel.split("/");
|
|
||||||
if (channels.indexOf(this.primaryChannel.value) == -1) return;
|
|
||||||
|
|
||||||
if (data.subscribed != undefined) this.handleSubscription(data.subscribed);
|
|
||||||
if (!data.messageBody) return;
|
|
||||||
|
|
||||||
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) return this.handleMessageObject(messageObject);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.handleMessage({
|
|
||||||
sender,
|
|
||||||
body: await decryptString(body, this.encryptionKey.value),
|
|
||||||
channel,
|
|
||||||
isoDate,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
handleSubscription = (isSubscribed: boolean): void => {
|
|
||||||
this.isSubscribed.value = isSubscribed;
|
|
||||||
|
|
||||||
if (isSubscribed == true) {
|
|
||||||
this.sendMessagesInOutbox();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
handleMessage = (chatMessage: ChatMessage): void => {
|
|
||||||
this.messages.add(chatMessage);
|
|
||||||
if (selectedChat.value != this) this.hasUnreadMessages.value = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
handleMessageObject = (messageObject: MessageObject): void => {
|
|
||||||
this.addObject(messageObject, true);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 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);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
updateOutboxCount() {
|
|
||||||
outboxItemCount.value =
|
|
||||||
this.outbox.value.size + this.objectOutbox.value.size;
|
|
||||||
}
|
|
||||||
|
|
||||||
// messages
|
|
||||||
createChatMessage = async (
|
|
||||||
messageText: string,
|
|
||||||
messageObject?: MessageObject
|
|
||||||
): Promise<ChatMessage> => {
|
|
||||||
// get channels
|
|
||||||
const secondaryChannelNames: string[] = [
|
|
||||||
...this.secondaryChannels.value.values(),
|
|
||||||
];
|
|
||||||
const allChannelNames: string[] = [
|
|
||||||
this.primaryChannel.value,
|
|
||||||
...secondaryChannelNames,
|
|
||||||
];
|
|
||||||
|
|
||||||
// create object
|
|
||||||
const joinedChannelName = allChannelNames.join("/");
|
|
||||||
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;
|
|
||||||
};
|
|
||||||
|
|
||||||
sendMessageFromComposer = async (): Promise<void> => {
|
|
||||||
if (this.cannotSendMessage.value == true) return;
|
|
||||||
|
|
||||||
await this.sendMessageFromText(this.composingMessage.value);
|
|
||||||
this.composingMessage.value = "";
|
|
||||||
};
|
|
||||||
|
|
||||||
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 = (chatMessage: ChatMessage): void => {
|
|
||||||
this.messages.remove(chatMessage);
|
|
||||||
};
|
|
||||||
|
|
||||||
deleteOutboxMessage = (chatMessage: ChatMessage): void => {
|
|
||||||
this.outbox.remove(chatMessage);
|
|
||||||
};
|
|
||||||
|
|
||||||
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 = (): MessageObjectContent => {
|
|
||||||
return {
|
|
||||||
id: React.UUID(),
|
|
||||||
isoDateVersionCreated: new Date().toISOString(),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
addObjectContent = (
|
|
||||||
messageObject: MessageObject,
|
|
||||||
content: MessageObjectContent
|
|
||||||
): void => {
|
|
||||||
messageObject.contentVersions[content.id] = content;
|
|
||||||
};
|
|
||||||
|
|
||||||
updateObjectContent = (
|
|
||||||
messageObject: MessageObject,
|
|
||||||
content: MessageObjectContent
|
|
||||||
): void => {
|
|
||||||
const latest = this.getMostRecentContent(messageObject);
|
|
||||||
Object.entries(content).forEach((entry) => {
|
|
||||||
const [key, value] = entry;
|
|
||||||
latest[key] = value;
|
|
||||||
});
|
|
||||||
this.addObjectContent(messageObject, latest);
|
|
||||||
};
|
|
||||||
|
|
||||||
addObject = (
|
|
||||||
messageObject: MessageObject,
|
|
||||||
ignoreIfUnchanged: boolean = false
|
|
||||||
): void => {
|
|
||||||
const existingObject = this.objects.value.get(messageObject.id);
|
|
||||||
if (existingObject) {
|
|
||||||
let isIdentical = existingObject.title == messageObject.title;
|
|
||||||
|
|
||||||
existingObject.title = messageObject.title;
|
|
||||||
Object.values(messageObject.contentVersions).forEach((content) => {
|
|
||||||
if (existingObject.contentVersions[content.id]) return;
|
|
||||||
this.addObjectContent(existingObject, content);
|
|
||||||
isIdentical = false;
|
|
||||||
});
|
|
||||||
if (isIdentical && ignoreIfUnchanged) return;
|
|
||||||
this.objects.set(existingObject.id, existingObject);
|
|
||||||
} else {
|
|
||||||
this.objects.set(messageObject.id, messageObject);
|
|
||||||
}
|
|
||||||
|
|
||||||
const latest = this.getMostRecentContent(messageObject);
|
|
||||||
if (latest.categoryName) usedObjectCategories.add(latest.categoryName);
|
|
||||||
if (latest.status) usedObjectStatuses.add(latest.status);
|
|
||||||
};
|
|
||||||
|
|
||||||
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 ? -1 : 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];
|
|
||||||
};
|
|
||||||
|
|
||||||
getObjectTitle = (messageObject: MessageObject) => {
|
|
||||||
return messageObject.title || translation.untitledObject;
|
|
||||||
};
|
|
||||||
|
|
||||||
// channel
|
|
||||||
setChannel = (): void => {
|
|
||||||
if (this.cannotSetChannel.value == true) return;
|
|
||||||
this.primaryChannel.value = this.primaryChannelInput.value;
|
|
||||||
UDN.subscribe(this.primaryChannel.value);
|
|
||||||
updateMailbox();
|
|
||||||
};
|
|
||||||
|
|
||||||
undoChannelChange = (): void => {
|
|
||||||
this.primaryChannelInput.value = this.primaryChannel.value;
|
|
||||||
};
|
|
||||||
|
|
||||||
addSecondaryChannel = (): void => {
|
|
||||||
if (this.cannotAddSecondaryChannel.value == true) return;
|
|
||||||
|
|
||||||
this.secondaryChannels.add(this.newSecondaryChannelName.value);
|
|
||||||
this.newSecondaryChannelName.value = "";
|
|
||||||
};
|
|
||||||
|
|
||||||
removeSecondaryChannel = (channel: string): void => {
|
|
||||||
this.secondaryChannels.remove(channel);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// METHODS
|
|
||||||
export function createChatWithName(name: string): void {
|
|
||||||
const newChat = new Chat();
|
|
||||||
chats.add(newChat);
|
|
||||||
|
|
||||||
newChat.primaryChannel.value = name;
|
|
||||||
newChat.primaryChannelInput.value = name;
|
|
||||||
chatIds.add(newChat.id);
|
|
||||||
|
|
||||||
UDN.subscribe(name);
|
|
||||||
updateMailbox();
|
|
||||||
}
|
|
||||||
@@ -1,283 +0,0 @@
|
|||||||
import * as React from "bloatless-react";
|
|
||||||
|
|
||||||
import { Chat, createChatWithName } from "./chatModel";
|
|
||||||
|
|
||||||
import UDNFrontend from "udn-frontend";
|
|
||||||
import { translation } from "../translations";
|
|
||||||
|
|
||||||
export const UDN = new UDNFrontend();
|
|
||||||
|
|
||||||
// CONNECTION
|
|
||||||
export const isConnected = new React.State(false);
|
|
||||||
|
|
||||||
export const serverAddressInput = React.restoreState("socket-address", "");
|
|
||||||
export const didRequestConnection = React.restoreState(
|
|
||||||
"did-request-connection",
|
|
||||||
false
|
|
||||||
);
|
|
||||||
export const currentAddress = React.restoreState("current-address", "");
|
|
||||||
export const previousAddresses =
|
|
||||||
React.restoreListState<string>("previous-addresses");
|
|
||||||
|
|
||||||
export const cannotDisonnect = React.createProxyState(
|
|
||||||
[isConnected],
|
|
||||||
() => isConnected.value == false
|
|
||||||
);
|
|
||||||
|
|
||||||
export const cannotConnect = React.createProxyState(
|
|
||||||
[serverAddressInput, currentAddress, isConnected],
|
|
||||||
() =>
|
|
||||||
serverAddressInput.value == "" ||
|
|
||||||
(currentAddress.value == serverAddressInput.value &&
|
|
||||||
isConnected.value == true)
|
|
||||||
);
|
|
||||||
|
|
||||||
export const cannotResetAddress = React.createProxyState(
|
|
||||||
[serverAddressInput, currentAddress],
|
|
||||||
() => serverAddressInput.value == currentAddress.value
|
|
||||||
);
|
|
||||||
|
|
||||||
// methods
|
|
||||||
export function connect(): void {
|
|
||||||
if (cannotConnect.value == true) return;
|
|
||||||
|
|
||||||
serverAddressInput.value = serverAddressInput.value.toLowerCase();
|
|
||||||
currentAddress.value = serverAddressInput.value;
|
|
||||||
|
|
||||||
isConnected.value = false;
|
|
||||||
didRequestConnection.value = true;
|
|
||||||
|
|
||||||
UDN.connect(serverAddressInput.value);
|
|
||||||
|
|
||||||
isMailboxActive.value = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function disconnect(): void {
|
|
||||||
didRequestConnection.value = false;
|
|
||||||
if (cannotDisonnect.value == true) return;
|
|
||||||
UDN.disconnect();
|
|
||||||
}
|
|
||||||
|
|
||||||
export function resetAddressInput(): void {
|
|
||||||
serverAddressInput.value = currentAddress.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function subscribeChannels() {
|
|
||||||
chats.value.forEach((chat) => {
|
|
||||||
UDN.subscribe(chat.primaryChannel.value);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// listeners
|
|
||||||
UDN.onconnect = () => {
|
|
||||||
isConnected.value = true;
|
|
||||||
const lowerCasedAddress = currentAddress.value.toLowerCase();
|
|
||||||
if (!previousAddresses.value.has(lowerCasedAddress)) {
|
|
||||||
previousAddresses.add(lowerCasedAddress);
|
|
||||||
}
|
|
||||||
|
|
||||||
subscribeChannels();
|
|
||||||
if (mailboxId.value != "") UDN.connectMailbox(mailboxId.value);
|
|
||||||
};
|
|
||||||
|
|
||||||
UDN.onmessage = (data) => {
|
|
||||||
chats.value.forEach((chat) => {
|
|
||||||
chat.onmessage(data);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
UDN.ondisconnect = () => {
|
|
||||||
isConnected.value = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
// MAILBOX
|
|
||||||
export const mailboxId = React.restoreState("mailbox-id", "");
|
|
||||||
export const isMailboxActive = new React.State(false);
|
|
||||||
|
|
||||||
export const cannotDeleteMailbox = React.createProxyState(
|
|
||||||
[mailboxId, isMailboxActive, isConnected],
|
|
||||||
() =>
|
|
||||||
mailboxId.value == "" ||
|
|
||||||
isMailboxActive.value == false ||
|
|
||||||
isConnected.value == false
|
|
||||||
);
|
|
||||||
export const cannotRequestMailbox = React.createProxyState(
|
|
||||||
[isConnected, isMailboxActive],
|
|
||||||
() => isConnected.value == false || isMailboxActive.value == true
|
|
||||||
);
|
|
||||||
|
|
||||||
export function requestMailbox(): void {
|
|
||||||
if (cannotRequestMailbox.value == true) return;
|
|
||||||
UDN.requestMailbox();
|
|
||||||
}
|
|
||||||
|
|
||||||
export function deleteMailbox(): void {
|
|
||||||
if (cannotDeleteMailbox.value == true) return;
|
|
||||||
UDN.deleteMailbox(mailboxId.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function updateMailbox(): void {
|
|
||||||
if (isMailboxActive.value == false) return;
|
|
||||||
deleteMailbox();
|
|
||||||
setTimeout(() => requestMailbox(), 10);
|
|
||||||
}
|
|
||||||
|
|
||||||
UDN.onmailboxcreate = (id) => {
|
|
||||||
mailboxId.value = id;
|
|
||||||
UDN.connectMailbox(id);
|
|
||||||
};
|
|
||||||
|
|
||||||
UDN.onmailboxconnect = (id) => {
|
|
||||||
isMailboxActive.value = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
UDN.onmailboxdelete = () => {
|
|
||||||
isMailboxActive.value = false;
|
|
||||||
mailboxId.value = "";
|
|
||||||
subscribeChannels();
|
|
||||||
};
|
|
||||||
|
|
||||||
// OUTBOX
|
|
||||||
export const outboxItemCount = new React.State(0);
|
|
||||||
export const outboxText = React.createProxyState([outboxItemCount], () =>
|
|
||||||
translation.outboxText(outboxItemCount.value)
|
|
||||||
);
|
|
||||||
export const outboxTextStyle = React.createProxyState([outboxItemCount], () =>
|
|
||||||
outboxItemCount.value == 0 ? "success" : "warning"
|
|
||||||
);
|
|
||||||
|
|
||||||
// MISC
|
|
||||||
export const isPresentingSettingsModal = new React.State(false);
|
|
||||||
export const isEncryptionAvailable = window.crypto.subtle != undefined;
|
|
||||||
export const senderName = React.restoreState("sender-name", "");
|
|
||||||
export const pageZoom = React.restoreState("page-zoom", 100);
|
|
||||||
export const objectFilterInput = React.restoreState("object-filter", "");
|
|
||||||
export const firstDayOfWeek = React.restoreState("first-day-of-week", 0);
|
|
||||||
export const dayInCalendar = React.restoreState("calendar-day", new Date().toISOString().split("T")[0])
|
|
||||||
|
|
||||||
pageZoom.subscribe(() => {
|
|
||||||
document.body.style.zoom = `${pageZoom.value}%`;
|
|
||||||
document.body.style.webkitTextSizeAdjust = `${pageZoom.value}%`;
|
|
||||||
});
|
|
||||||
|
|
||||||
export const usedObjectCategories =
|
|
||||||
React.restoreListState<string>("object-categories");
|
|
||||||
export const usedObjectStatuses =
|
|
||||||
React.restoreListState<string>("object-statuses");
|
|
||||||
export const previousObjectSearches =
|
|
||||||
React.restoreListState<string>("object-searches");
|
|
||||||
|
|
||||||
export function toggleSettings() {
|
|
||||||
isPresentingSettingsModal.value = !isPresentingSettingsModal.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function repairApp() {
|
|
||||||
mapSetToLowercase(previousAddresses);
|
|
||||||
removeSetDuplicates(previousAddresses);
|
|
||||||
removeSetDuplicates(usedObjectCategories);
|
|
||||||
removeSetDuplicates(usedObjectStatuses);
|
|
||||||
removeSetDuplicates(previousObjectSearches);
|
|
||||||
}
|
|
||||||
|
|
||||||
function mapSetToLowercase(listState: React.ListState<string>) {
|
|
||||||
const lowerCased = [...listState.value].map((x) => x.toLowerCase());
|
|
||||||
listState.clear();
|
|
||||||
listState.add(...lowerCased);
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeSetDuplicates(listState: React.ListState<any>) {
|
|
||||||
const values = [...listState.value];
|
|
||||||
listState.clear();
|
|
||||||
listState.add(...values);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function clearAddresses() {
|
|
||||||
previousAddresses.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
export function clearCategories() {
|
|
||||||
usedObjectCategories.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
export function clearStatuses() {
|
|
||||||
usedObjectStatuses.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
export function clearObjectSearches() {
|
|
||||||
previousObjectSearches.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
const zoomStep = 10;
|
|
||||||
export function zoomOut() {
|
|
||||||
pageZoom.value -= zoomStep;
|
|
||||||
}
|
|
||||||
export function zoomIn() {
|
|
||||||
pageZoom.value += zoomStep;
|
|
||||||
}
|
|
||||||
|
|
||||||
// CHAT
|
|
||||||
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 isShowingObjects = React.restoreState("showing-objects", false);
|
|
||||||
export const isShowingChatInSplit = React.restoreState(
|
|
||||||
"showing-chat-split",
|
|
||||||
false
|
|
||||||
);
|
|
||||||
export const isChatOpen = React.createProxyState(
|
|
||||||
[selectedChat],
|
|
||||||
() => selectedChat.value != undefined
|
|
||||||
);
|
|
||||||
export const isChatVisible = React.createProxyState(
|
|
||||||
[isShowingObjects, isShowingChatInSplit],
|
|
||||||
() => isShowingObjects.value == false || isShowingChatInSplit.value == true
|
|
||||||
);
|
|
||||||
isShowingObjects.subscribeSilent(() => {
|
|
||||||
setTimeout(() => scrollToChat("instant"), 1);
|
|
||||||
});
|
|
||||||
|
|
||||||
export const newChatName = new React.State("");
|
|
||||||
|
|
||||||
export const cannotCreateChat = React.createProxyState(
|
|
||||||
[newChatName],
|
|
||||||
() => newChatName.value == ""
|
|
||||||
);
|
|
||||||
|
|
||||||
export function createChat() {
|
|
||||||
if (cannotCreateChat.value == true) return;
|
|
||||||
|
|
||||||
createChatWithName(newChatName.value);
|
|
||||||
newChatName.value = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
export function closeChatView() {
|
|
||||||
selectedChat.value = undefined;
|
|
||||||
document.getElementById("settings-tab")?.scrollIntoView();
|
|
||||||
}
|
|
||||||
|
|
||||||
export function selectChat(chat: Chat) {
|
|
||||||
selectedChat.value = chat;
|
|
||||||
chat.hasUnreadMessages.value = false;
|
|
||||||
scrollToChat();
|
|
||||||
}
|
|
||||||
|
|
||||||
export function scrollToChat(behavior: ScrollBehavior = "smooth") {
|
|
||||||
document.getElementById("message-tab")?.scrollIntoView({ behavior });
|
|
||||||
}
|
|
||||||
|
|
||||||
export function toggleObjects() {
|
|
||||||
isShowingObjects.value = !isShowingObjects.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function toggleChat() {
|
|
||||||
isShowingChatInSplit.value = !isShowingChatInSplit.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
chatIds.value.forEach((id) => chats.add(new Chat(id)));
|
|
||||||
|
|
||||||
// INIT
|
|
||||||
if (serverAddressInput.value != "" && didRequestConnection.value == true) {
|
|
||||||
connect();
|
|
||||||
}
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
import * as React from "bloatless-react";
|
|
||||||
|
|
||||||
import {
|
|
||||||
closeChatView,
|
|
||||||
isChatOpen,
|
|
||||||
isChatVisible,
|
|
||||||
isShowingObjects,
|
|
||||||
selectedChat,
|
|
||||||
toggleChat,
|
|
||||||
toggleObjects,
|
|
||||||
} from "../Model/model";
|
|
||||||
|
|
||||||
import { ChatOptionModal } from "../Views/Chat/chatOptionModal";
|
|
||||||
import { MessageComposer } from "../Views/Chat/messageComposer";
|
|
||||||
import { ObjectPane } from "../Views/Objects/objectPane";
|
|
||||||
import { ThreadView } from "../Views/Chat/threadView";
|
|
||||||
import { translation } from "../translations";
|
|
||||||
|
|
||||||
export function MessageTab() {
|
|
||||||
const messageTabContent = React.createProxyState([selectedChat], () => {
|
|
||||||
if (selectedChat.value == undefined)
|
|
||||||
return (
|
|
||||||
<div class="width-100 height-100 flex-column align-center justify-center">
|
|
||||||
<span class="secondary">{translation.noChatSelected}</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
const chat = selectedChat.value;
|
|
||||||
const isShowingOptions = new React.State(false);
|
|
||||||
|
|
||||||
function showOptions() {
|
|
||||||
isShowingOptions.value = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
|
||||||
<header class="padding-0">
|
|
||||||
<span class="flex-row align-center">
|
|
||||||
<button aria-label={translation.back} on:click={closeChatView}>
|
|
||||||
<span class="icon">arrow_back</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<span subscribe:innerText={chat.primaryChannel}></span>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<button
|
|
||||||
aria-label={translation.showObjects}
|
|
||||||
on:click={toggleObjects}
|
|
||||||
toggle:selected={isShowingObjects}
|
|
||||||
>
|
|
||||||
<span class="icon">deployed_code</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
aria-label={translation.showObjects} // TODO
|
|
||||||
on:click={toggleChat}
|
|
||||||
toggle:selected={isChatVisible}
|
|
||||||
>
|
|
||||||
<span class="icon">forum</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
aria-label={translation.showChatOptions}
|
|
||||||
on:click={showOptions}
|
|
||||||
>
|
|
||||||
<span class="icon">tune</span>
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
</header>,
|
|
||||||
|
|
||||||
ObjectPane(chat),
|
|
||||||
ThreadView(chat),
|
|
||||||
|
|
||||||
<footer>{MessageComposer(chat)}</footer>,
|
|
||||||
|
|
||||||
ChatOptionModal(chat, isShowingOptions),
|
|
||||||
];
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<article
|
|
||||||
id="message-tab"
|
|
||||||
children:set={messageTabContent}
|
|
||||||
toggle:showingobjects={isShowingObjects}
|
|
||||||
toggle:chatvisible={isChatVisible}
|
|
||||||
toggle:chatopen={isChatOpen}
|
|
||||||
></article>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
import * as React from "bloatless-react";
|
|
||||||
|
|
||||||
import { isConnected, isEncryptionAvailable, toggleSettings, zoomIn, zoomOut } from "../Model/model";
|
|
||||||
|
|
||||||
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() {
|
|
||||||
return (
|
|
||||||
<article id="settings-tab" toggle:connected={isConnected}>
|
|
||||||
<header>
|
|
||||||
{translation.overview}
|
|
||||||
<button aria-label={translation.showSettings} on:click={toggleSettings}>
|
|
||||||
<span class="icon">settings</span>
|
|
||||||
</button>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div class="flex-column large-gap">
|
|
||||||
<div class="tile error flex-no" toggle:hidden={isEncryptionAvailable}>
|
|
||||||
<span class="icon">warning</span>
|
|
||||||
<div>
|
|
||||||
<b>{translation.encryptionUnavailableTitle}</b>
|
|
||||||
<span class="secondary">
|
|
||||||
{translation.encryptionUnavailableMessage}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{PersonalSection()}
|
|
||||||
{ConnectionSection()}
|
|
||||||
{ChatListSection()}
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,175 +0,0 @@
|
|||||||
import * as React from "bloatless-react";
|
|
||||||
|
|
||||||
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() {
|
|
||||||
isPresented.value = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function deleteChat() {
|
|
||||||
chat.deleteSelf();
|
|
||||||
closeModal();
|
|
||||||
closeChatView();
|
|
||||||
}
|
|
||||||
|
|
||||||
const shouldShowKey = new React.State(false);
|
|
||||||
const inputType = React.createProxyState([shouldShowKey], () =>
|
|
||||||
shouldShowKey.value == true ? "text" : "password"
|
|
||||||
);
|
|
||||||
|
|
||||||
const secondaryChannelConverter: React.StateItemConverter<string> = (
|
|
||||||
secondaryChannel
|
|
||||||
) => {
|
|
||||||
function remove() {
|
|
||||||
chat.removeSecondaryChannel(secondaryChannel);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div class="tile width-input padding-0">
|
|
||||||
<div class="flex-row justify-apart align-center">
|
|
||||||
<b class="padding-h">{secondaryChannel}</b>
|
|
||||||
<button
|
|
||||||
class="danger"
|
|
||||||
aria-label={translation.removeSecondaryChannel}
|
|
||||||
on:click={remove}
|
|
||||||
>
|
|
||||||
<span class="icon">delete</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div class="modal" toggle:open={isPresented}>
|
|
||||||
<div>
|
|
||||||
<main>
|
|
||||||
<h2>{translation.configureChatTitle}</h2>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label class="tile">
|
|
||||||
<span class="icon">forum</span>
|
|
||||||
<div>
|
|
||||||
<span>{translation.primaryChannel}</span>
|
|
||||||
<input
|
|
||||||
bind:value={chat.primaryChannelInput}
|
|
||||||
placeholder={translation.primaryChannelPlaceholder}
|
|
||||||
on:enter={chat.setChannel}
|
|
||||||
></input>
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
<div class="flex-row width-input">
|
|
||||||
<button
|
|
||||||
aria-label={translation.undoChanges}
|
|
||||||
class="flex justify-center"
|
|
||||||
on:click={chat.undoChannelChange}
|
|
||||||
toggle:disabled={chat.cannotUndoChannel}
|
|
||||||
>
|
|
||||||
<span class="icon">undo</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
aria-label={translation.set}
|
|
||||||
class="flex justify-center primary"
|
|
||||||
on:click={chat.setChannel}
|
|
||||||
toggle:disabled={chat.cannotSetChannel}
|
|
||||||
>
|
|
||||||
<span class="icon">check</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<hr></hr>
|
|
||||||
|
|
||||||
<div class="flex-row margin-bottom width-input">
|
|
||||||
<input
|
|
||||||
aria-label={translation.secondaryChannel}
|
|
||||||
placeholder={translation.secondaryChannelPlaceholder}
|
|
||||||
bind:value={chat.newSecondaryChannelName}
|
|
||||||
on:enter={chat.addSecondaryChannel}
|
|
||||||
></input>
|
|
||||||
<button
|
|
||||||
class="primary"
|
|
||||||
toggle:disabled={chat.cannotAddSecondaryChannel}
|
|
||||||
aria-label={translation.addSecondaryChannel}
|
|
||||||
on:click={chat.addSecondaryChannel}
|
|
||||||
>
|
|
||||||
<span class="icon">add</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="flex-column gap"
|
|
||||||
children:prepend={[
|
|
||||||
chat.secondaryChannels,
|
|
||||||
secondaryChannelConverter,
|
|
||||||
]}
|
|
||||||
></div>
|
|
||||||
|
|
||||||
<hr></hr>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label class="tile">
|
|
||||||
<span class="icon">key</span>
|
|
||||||
<div>
|
|
||||||
<span>{translation.encryptionKey}</span>
|
|
||||||
<input
|
|
||||||
placeholder={translation.encryptionKeyPlaceholder}
|
|
||||||
bind:value={chat.encryptionKey}
|
|
||||||
set:type={inputType}
|
|
||||||
></input>
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label class="inline margin-0">
|
|
||||||
<input type="checkbox" bind:checked={shouldShowKey}></input>
|
|
||||||
{translation.showKey}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<hr></hr>
|
|
||||||
|
|
||||||
<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">chat_error</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button class="danger" on:click={deleteChat}>
|
|
||||||
{translation.removeChat}
|
|
||||||
<span class="icon">delete_forever</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
<button on:click={closeModal}>
|
|
||||||
{translation.close}
|
|
||||||
<span class="icon">close</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
import * as React from "bloatless-react";
|
|
||||||
|
|
||||||
import { Chat } from "../../Model/chatModel";
|
|
||||||
import { translation } from "../../translations";
|
|
||||||
|
|
||||||
export function MessageComposer(chat: Chat) {
|
|
||||||
return (
|
|
||||||
<div class="flex-row width-100">
|
|
||||||
{" "}
|
|
||||||
<input
|
|
||||||
class="width-100 flex-1"
|
|
||||||
style="max-width: unset"
|
|
||||||
placeholder={translation.composerPlaceholder}
|
|
||||||
bind:value={chat.composingMessage}
|
|
||||||
on:enter={chat.sendMessageFromComposer}
|
|
||||||
></input>
|
|
||||||
<button
|
|
||||||
class="primary"
|
|
||||||
on:click={chat.sendMessageFromComposer}
|
|
||||||
toggle:disabled={chat.cannotSendMessage}
|
|
||||||
>
|
|
||||||
<span class="icon">send</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,146 +0,0 @@
|
|||||||
import * as React from "bloatless-react";
|
|
||||||
|
|
||||||
import { Chat, ChatMessage } from "../../Model/chatModel";
|
|
||||||
|
|
||||||
import { MessageComposer } from "./messageComposer";
|
|
||||||
import { isShowingObjects } from "../../Model/model";
|
|
||||||
import { translation } from "../../translations";
|
|
||||||
|
|
||||||
export function ThreadView(chat: Chat) {
|
|
||||||
const messageConverter: React.StateItemConverter<ChatMessage> = (message) => {
|
|
||||||
function resendMessage() {
|
|
||||||
chat.resendMessage(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
function copyMessage() {
|
|
||||||
navigator.clipboard.writeText(message.body);
|
|
||||||
}
|
|
||||||
|
|
||||||
function decrypt() {
|
|
||||||
chat.decryptReceivedMessage(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
function remove() {
|
|
||||||
chat.deleteMessage(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
const messageBody = React.createProxyState(
|
|
||||||
[chat.messages],
|
|
||||||
() => message.body
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div class="tile width-100 flex-no padding-0">
|
|
||||||
<div class="flex-column">
|
|
||||||
<div class="flex-row justify-apart align-center secondary">
|
|
||||||
<span class="padding-h ellipsis">
|
|
||||||
<b class="info">{message.sender}</b>
|
|
||||||
</span>
|
|
||||||
<span class="flex-row">
|
|
||||||
<button
|
|
||||||
aria-label={translation.resendMessage}
|
|
||||||
on:click={resendMessage}
|
|
||||||
toggle:disabled={chat.cannotResendMessage}
|
|
||||||
>
|
|
||||||
<span class="icon">replay</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
aria-label={translation.copyMessage}
|
|
||||||
on:click={copyMessage}
|
|
||||||
>
|
|
||||||
<span class="icon">content_copy</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
aria-label={translation.decryptMessage}
|
|
||||||
on:click={decrypt}
|
|
||||||
>
|
|
||||||
<span class="icon">key</span>
|
|
||||||
</button>
|
|
||||||
<button aria-label={translation.deleteMessage} on:click={remove}>
|
|
||||||
<span class="icon">delete</span>
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex-column padding-h padding-bottom">
|
|
||||||
<b class="break-word" subscribe:innerText={messageBody}></b>
|
|
||||||
<span class="secondary">
|
|
||||||
<b>{new Date(message.isoDate).toLocaleString()}</b>
|
|
||||||
<br></br>
|
|
||||||
{message.channel}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const outboxMessageConverter: React.StateItemConverter<ChatMessage> = (
|
|
||||||
message
|
|
||||||
) => {
|
|
||||||
function remove() {
|
|
||||||
chat.deleteOutboxMessage(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div class="tile width-100 flex-no padding-0">
|
|
||||||
<div class="flex-column">
|
|
||||||
<div class="flex-row justify-apart align-center">
|
|
||||||
<b class="error padding-h">{translation.messageInOutbox}</b>
|
|
||||||
<span class="flex-row secondary">
|
|
||||||
<button aria-label={translation.deleteMessage} on:click={remove}>
|
|
||||||
<span class="icon">delete</span>
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex-column padding-h padding-bottom">
|
|
||||||
<b class="break-word">{message.body}</b>
|
|
||||||
<span class="secondary">
|
|
||||||
<b>{new Date(message.isoDate).toLocaleString()}</b>
|
|
||||||
<br></br>
|
|
||||||
{message.channel}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const messageList = (
|
|
||||||
<div
|
|
||||||
class="flex-column gap"
|
|
||||||
children:append={[chat.messages, messageConverter]}
|
|
||||||
></div>
|
|
||||||
);
|
|
||||||
const outboxList = (
|
|
||||||
<div
|
|
||||||
class="flex-column gap"
|
|
||||||
children:append={[chat.outbox, outboxMessageConverter]}
|
|
||||||
></div>
|
|
||||||
);
|
|
||||||
const listWrapper = (
|
|
||||||
<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;
|
|
||||||
|
|
||||||
scrollToBottom();
|
|
||||||
}
|
|
||||||
|
|
||||||
chat.messages.handleAddition(scrollToBottomIfAppropriate);
|
|
||||||
chat.outbox.handleAddition(scrollToBottomIfAppropriate);
|
|
||||||
isShowingObjects.subscribe(() => scrollToBottom());
|
|
||||||
setTimeout(() => scrollToBottom(), 50);
|
|
||||||
|
|
||||||
return listWrapper;
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
import * as React from "bloatless-react";
|
|
||||||
|
|
||||||
import { Chat, MessageObject, MessageObjectWithIndex } from "../../Model/chatModel";
|
|
||||||
|
|
||||||
import { ObjectGridView } from "./objectGridView";
|
|
||||||
import { translation } from "../../translations";
|
|
||||||
|
|
||||||
export function AllObjectsView(
|
|
||||||
chat: Chat,
|
|
||||||
messageObjects: React.MapState<MessageObjectWithIndex>,
|
|
||||||
selectedObject: React.State<MessageObject | undefined>,
|
|
||||||
isShowingObjectModal: React.State<boolean>
|
|
||||||
) {
|
|
||||||
const objectCount = React.createProxyState(
|
|
||||||
[chat.objects],
|
|
||||||
() => chat.objects.value.size
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div class="width-100 height-100 scroll-v padding flex-column gap">
|
|
||||||
<b class="secondary">
|
|
||||||
{translation.objectsInTotal}{" "}
|
|
||||||
<span subscribe:innerText={objectCount}></span>
|
|
||||||
</b>
|
|
||||||
{ObjectGridView(
|
|
||||||
chat,
|
|
||||||
messageObjects,
|
|
||||||
selectedObject,
|
|
||||||
isShowingObjectModal
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,133 +0,0 @@
|
|||||||
import * as React from "bloatless-react";
|
|
||||||
|
|
||||||
import {
|
|
||||||
Chat,
|
|
||||||
MessageObject,
|
|
||||||
MessageObjectWithIndex,
|
|
||||||
} from "../../Model/chatModel";
|
|
||||||
import { dayInCalendar, firstDayOfWeek } from "../../Model/model";
|
|
||||||
|
|
||||||
import { DayView } from "./dayView";
|
|
||||||
import { MonthGridCell } from "./monthGridCell";
|
|
||||||
import { translation } from "../../translations";
|
|
||||||
|
|
||||||
export function CalendarView(
|
|
||||||
chat: Chat,
|
|
||||||
messageObjects: React.MapState<MessageObjectWithIndex>,
|
|
||||||
selectedObject: React.State<MessageObject | undefined>,
|
|
||||||
isShowingObjectModal: React.State<boolean>
|
|
||||||
) {
|
|
||||||
let selectedDate = new Date(dayInCalendar.value);
|
|
||||||
const selectedMonth = new React.State(selectedDate.getUTCMonth() + 1);
|
|
||||||
const selectedYear = new React.State(selectedDate.getUTCFullYear());
|
|
||||||
|
|
||||||
React.bulkSubscribe([selectedMonth, selectedYear], () =>
|
|
||||||
updateSelectedDate()
|
|
||||||
);
|
|
||||||
dayInCalendar.subscribeSilent(() => {
|
|
||||||
selectedDate = new Date(dayInCalendar.value);
|
|
||||||
});
|
|
||||||
updateSelectedDate();
|
|
||||||
|
|
||||||
function updateSelectedDate() {
|
|
||||||
selectedDate.setUTCMonth(selectedMonth.value - 1);
|
|
||||||
selectedDate.setUTCFullYear(selectedYear.value);
|
|
||||||
dayInCalendar.value = selectedDate.toISOString().split("T")[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
function previousMonth() {
|
|
||||||
if (selectedMonth.value <= 1) {
|
|
||||||
selectedMonth.value = 12;
|
|
||||||
selectedYear.value -= 1;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
selectedMonth.value -= 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
function nextMonth() {
|
|
||||||
if (selectedMonth.value >= 12) {
|
|
||||||
selectedMonth.value = 1;
|
|
||||||
selectedYear.value += 1;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
selectedMonth.value += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
let monthGridCells = new React.State<HTMLElement[]>([]);
|
|
||||||
React.bulkSubscribe([dayInCalendar, firstDayOfWeek], updateMonthGrid);
|
|
||||||
updateMonthGrid();
|
|
||||||
|
|
||||||
function updateMonthGrid() {
|
|
||||||
const newSelectedDate = new Date(dayInCalendar.value);
|
|
||||||
|
|
||||||
const currentDate = new Date();
|
|
||||||
monthGridCells.value = [];
|
|
||||||
|
|
||||||
// initialize to first day of month
|
|
||||||
currentDate.setUTCDate(1);
|
|
||||||
currentDate.setUTCMonth(newSelectedDate.getMonth());
|
|
||||||
|
|
||||||
// labels
|
|
||||||
let currentLabelIndex = firstDayOfWeek.value;
|
|
||||||
for (let i = 0; i < 7; i++) {
|
|
||||||
monthGridCells.value.push(
|
|
||||||
<b class="secondary ellipsis width-100">
|
|
||||||
{translation.weekdays[currentLabelIndex]}
|
|
||||||
</b>
|
|
||||||
);
|
|
||||||
currentLabelIndex++;
|
|
||||||
if (currentLabelIndex >= 7) currentLabelIndex = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// create blank dates starting sunday
|
|
||||||
const monthOffset = currentDate.getDay() - firstDayOfWeek.value;
|
|
||||||
for (let i = 0; i < monthOffset; i++) {
|
|
||||||
monthGridCells.value.push(<div></div>);
|
|
||||||
}
|
|
||||||
|
|
||||||
// add day views
|
|
||||||
while (currentDate.getUTCMonth() == newSelectedDate.getUTCMonth()) {
|
|
||||||
monthGridCells.value.push(
|
|
||||||
MonthGridCell(chat, messageObjects, currentDate)
|
|
||||||
);
|
|
||||||
currentDate.setDate(currentDate.getDate() + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
monthGridCells.callSubscriptions();
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div class="calendar-wrapper">
|
|
||||||
<div class="month-grid padding scroll-v">
|
|
||||||
<div class="flex-row justify-center gap">
|
|
||||||
<button
|
|
||||||
on:click={previousMonth}
|
|
||||||
aria-label={translation.previousMonth}
|
|
||||||
>
|
|
||||||
<span class="icon">arrow_back</span>
|
|
||||||
</button>
|
|
||||||
<input
|
|
||||||
style="width: 90px"
|
|
||||||
type="number"
|
|
||||||
bind:value={selectedMonth}
|
|
||||||
></input>
|
|
||||||
<input
|
|
||||||
style="width: 110px"
|
|
||||||
type="number"
|
|
||||||
bind:value={selectedYear}
|
|
||||||
></input>
|
|
||||||
<button on:click={nextMonth} aria-label={translation.nextMonth}>
|
|
||||||
<span class="icon">arrow_forward</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<hr></hr>
|
|
||||||
<div
|
|
||||||
class="grid gap"
|
|
||||||
style="grid-template-columns: repeat(7, 1fr)"
|
|
||||||
children:set={monthGridCells}
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
{DayView(chat, messageObjects, selectedObject, isShowingObjectModal)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
import * as React from "bloatless-react";
|
|
||||||
|
|
||||||
import { Chat, MessageObject, MessageObjectWithIndex } from "../../Model/chatModel";
|
|
||||||
|
|
||||||
import { ObjectEntryView } from "./objectEntryView";
|
|
||||||
import { dayInCalendar } from "../../Model/model";
|
|
||||||
|
|
||||||
export function DayView(
|
|
||||||
chat: Chat,
|
|
||||||
messageObjects:
|
|
||||||
| React.ListState<MessageObjectWithIndex>
|
|
||||||
| React.MapState<MessageObjectWithIndex>,
|
|
||||||
selectedObject: React.State<MessageObject | undefined>,
|
|
||||||
isShowingObjectModal: React.State<boolean>
|
|
||||||
) {
|
|
||||||
const objectsForDayView = new React.ListState<MessageObjectWithIndex>();
|
|
||||||
|
|
||||||
function processObject(messageObject: MessageObjectWithIndex) {
|
|
||||||
const latest = chat.getMostRecentContent(messageObject);
|
|
||||||
if (!latest.date || latest.date != dayInCalendar.value) return;
|
|
||||||
|
|
||||||
objectsForDayView.add(messageObject);
|
|
||||||
messageObjects.handleRemoval(messageObject, () => {
|
|
||||||
objectsForDayView.remove(messageObject);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
messageObjects.handleAddition(processObject);
|
|
||||||
dayInCalendar.subscribeSilent(() => {
|
|
||||||
objectsForDayView.clear();
|
|
||||||
messageObjects.value.forEach(processObject);
|
|
||||||
})
|
|
||||||
|
|
||||||
const objectConverter: React.StateItemConverter<MessageObjectWithIndex> = (
|
|
||||||
messageObject
|
|
||||||
) => {
|
|
||||||
return ObjectEntryView(
|
|
||||||
chat,
|
|
||||||
messageObject,
|
|
||||||
selectedObject,
|
|
||||||
isShowingObjectModal
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
class="day-view padding flex-column gap scroll-v"
|
|
||||||
children:append={[objectsForDayView, objectConverter]}
|
|
||||||
></div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,141 +0,0 @@
|
|||||||
import * as React from "bloatless-react";
|
|
||||||
|
|
||||||
import { Chat, MessageObject, MessageObjectWithIndex } from "../../Model/chatModel";
|
|
||||||
|
|
||||||
import { ObjectEntryView } from "./objectEntryView";
|
|
||||||
import { RenameView } from "../renameView";
|
|
||||||
|
|
||||||
interface KanbanBoard {
|
|
||||||
category: string;
|
|
||||||
items: React.ListState<KanbanBoardItem>;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface KanbanBoardItem {
|
|
||||||
priority: number;
|
|
||||||
messageObject: MessageObjectWithIndex;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function KanbanView(
|
|
||||||
chat: Chat,
|
|
||||||
messageObjects: React.MapState<MessageObjectWithIndex>,
|
|
||||||
selectedObject: React.State<MessageObject | undefined>,
|
|
||||||
isShowingObjectModal: React.State<boolean>
|
|
||||||
) {
|
|
||||||
const boards = new React.MapState<KanbanBoard>();
|
|
||||||
const sortedBoardNames = React.createProxyState([boards], () =>
|
|
||||||
[...boards.value.values()]
|
|
||||||
.map((board) => board.category)
|
|
||||||
.sort((a, b) => a.localeCompare(b))
|
|
||||||
);
|
|
||||||
|
|
||||||
messageObjects.handleAddition((messageObject) => {
|
|
||||||
const latest = chat.getMostRecentContent(messageObject);
|
|
||||||
if (!latest.categoryName) return;
|
|
||||||
|
|
||||||
const categoryName = latest.categoryName!;
|
|
||||||
if (!boards.value.has(categoryName)) {
|
|
||||||
const listState = new React.ListState<KanbanBoardItem>();
|
|
||||||
listState.subscribeSilent(() => {
|
|
||||||
if (listState.value.size != 0) return;
|
|
||||||
boards.remove(categoryName);
|
|
||||||
});
|
|
||||||
|
|
||||||
boards.set(categoryName, {
|
|
||||||
category: categoryName,
|
|
||||||
items: listState,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const boardItem: KanbanBoardItem = {
|
|
||||||
priority: latest.priority ?? 0,
|
|
||||||
messageObject,
|
|
||||||
};
|
|
||||||
boards.value.get(categoryName)?.items.add(boardItem);
|
|
||||||
|
|
||||||
messageObjects.handleRemoval(messageObject, () => {
|
|
||||||
boards.value.get(categoryName)?.items.remove(boardItem);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const boardToBoardView: React.StateItemConverter<KanbanBoard> = (board) => {
|
|
||||||
const view = KanbanBoardView(
|
|
||||||
chat,
|
|
||||||
board,
|
|
||||||
selectedObject,
|
|
||||||
isShowingObjectModal
|
|
||||||
);
|
|
||||||
sortedBoardNames.subscribe((sortedBoardNames) => {
|
|
||||||
view.style.order = sortedBoardNames.indexOf(board.category);
|
|
||||||
});
|
|
||||||
return view;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
class="flex-row large-gap width-100 height-100 scroll-v scroll-h padding"
|
|
||||||
children:append={[boards, boardToBoardView]}
|
|
||||||
></div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function KanbanBoardView(
|
|
||||||
chat: Chat,
|
|
||||||
kanbanBoard: KanbanBoard,
|
|
||||||
selectedObject: React.State<MessageObject | undefined>,
|
|
||||||
isShowingObjectModal: React.State<boolean>
|
|
||||||
) {
|
|
||||||
const editingCategory = new React.State(kanbanBoard.category);
|
|
||||||
|
|
||||||
function renameBoard() {
|
|
||||||
kanbanBoard.items.value.forEach((kanbanBoardItem) => {
|
|
||||||
const { messageObject } = kanbanBoardItem;
|
|
||||||
const latest = chat.getMostRecentContent(messageObject);
|
|
||||||
latest.categoryName = editingCategory.value;
|
|
||||||
chat.addObjectAndSend(messageObject);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function dragOver(event: DragEvent) {
|
|
||||||
event.preventDefault();
|
|
||||||
}
|
|
||||||
|
|
||||||
function drop(event: DragEvent) {
|
|
||||||
event.preventDefault();
|
|
||||||
var id = event.dataTransfer?.getData("text");
|
|
||||||
if (!id) return;
|
|
||||||
|
|
||||||
const messageObject = chat.objects.value.get(id);
|
|
||||||
if (!messageObject) return;
|
|
||||||
|
|
||||||
const newContent = chat.createObjectContent();
|
|
||||||
newContent.categoryName = kanbanBoard.category;
|
|
||||||
chat.updateObjectContent(messageObject, newContent);
|
|
||||||
chat.addObjectAndSend(messageObject);
|
|
||||||
}
|
|
||||||
|
|
||||||
const itemToViewEntry: React.StateItemConverter<KanbanBoardItem> = (
|
|
||||||
kanbanBoardItem
|
|
||||||
) => {
|
|
||||||
return ObjectEntryView(
|
|
||||||
chat,
|
|
||||||
kanbanBoardItem.messageObject,
|
|
||||||
selectedObject,
|
|
||||||
isShowingObjectModal
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
class="flex-column flex-no object-entry-wide"
|
|
||||||
on:dragover={dragOver}
|
|
||||||
on:drop={drop}
|
|
||||||
>
|
|
||||||
{RenameView(editingCategory, kanbanBoard.category, renameBoard)}
|
|
||||||
<hr></hr>
|
|
||||||
<div
|
|
||||||
class="flex-column gap padding-bottom"
|
|
||||||
children:append={[kanbanBoard.items, itemToViewEntry]}
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
import * as React from "bloatless-react";
|
|
||||||
|
|
||||||
import { Chat, MessageObject, MessageObjectWithIndex } from "../../Model/chatModel";
|
|
||||||
|
|
||||||
import { dayInCalendar } from "../../Model/model";
|
|
||||||
import { getRawObjectIndex } from "../../utility";
|
|
||||||
|
|
||||||
export function MonthGridCell(
|
|
||||||
chat: Chat,
|
|
||||||
messageObjects:
|
|
||||||
| React.ListState<MessageObjectWithIndex>
|
|
||||||
| React.MapState<MessageObjectWithIndex>,
|
|
||||||
dateObject: Date
|
|
||||||
) {
|
|
||||||
const objectsForDayView = new React.ListState<MessageObject>();
|
|
||||||
|
|
||||||
const dateString = dateObject.toISOString().split("T")[0];
|
|
||||||
|
|
||||||
function processObject(messageObject: MessageObjectWithIndex) {
|
|
||||||
const latest = chat.getMostRecentContent(messageObject);
|
|
||||||
if (!latest.date || latest.date != dateString) return;
|
|
||||||
|
|
||||||
objectsForDayView.add(messageObject);
|
|
||||||
messageObjects.handleRemoval(messageObject, () => {
|
|
||||||
objectsForDayView.remove(messageObject);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function select() {
|
|
||||||
dayInCalendar.value = dateString;
|
|
||||||
}
|
|
||||||
|
|
||||||
const isSelected = React.createProxyState(
|
|
||||||
[dayInCalendar],
|
|
||||||
() => dayInCalendar.value == dateString
|
|
||||||
);
|
|
||||||
const today = new Date();
|
|
||||||
const isToday =
|
|
||||||
today.getDate() == dateObject.getUTCDate() &&
|
|
||||||
today.getMonth() == dateObject.getUTCMonth() &&
|
|
||||||
today.getFullYear() == dateObject.getUTCFullYear();
|
|
||||||
|
|
||||||
messageObjects.handleAddition(processObject);
|
|
||||||
|
|
||||||
const objectConverter: React.StateItemConverter<MessageObjectWithIndex> = (
|
|
||||||
messageObject
|
|
||||||
) => {
|
|
||||||
const view = (
|
|
||||||
<span class="secondary ellipsis">
|
|
||||||
{chat.getObjectTitle(messageObject)}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
messageObject.index.subscribe(newIndex => {
|
|
||||||
view.style.order = newIndex;
|
|
||||||
})
|
|
||||||
return view;
|
|
||||||
};
|
|
||||||
|
|
||||||
function dragOver(event: DragEvent) {
|
|
||||||
event.preventDefault();
|
|
||||||
}
|
|
||||||
|
|
||||||
function drop(event: DragEvent) {
|
|
||||||
event.preventDefault();
|
|
||||||
var id = event.dataTransfer?.getData("text");
|
|
||||||
if (!id) return;
|
|
||||||
|
|
||||||
const messageObject = chat.objects.value.get(id);
|
|
||||||
if (!messageObject) return;
|
|
||||||
|
|
||||||
const newContent = chat.createObjectContent();
|
|
||||||
newContent.date = dateString;
|
|
||||||
chat.updateObjectContent(messageObject, newContent);
|
|
||||||
chat.addObjectAndSend(messageObject);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
on:click={select}
|
|
||||||
on:dragover={dragOver}
|
|
||||||
on:drop={drop}
|
|
||||||
toggle:selected={isSelected}
|
|
||||||
toggle:today={isToday}
|
|
||||||
class="day-miniature tile flex-column align-start"
|
|
||||||
style="aspect-ratio: 1/1; overflow: hidden"
|
|
||||||
>
|
|
||||||
<h3 class="margin-0">{dateObject.getUTCDate()}</h3>
|
|
||||||
<div
|
|
||||||
class="flex-column gap"
|
|
||||||
children:append={[objectsForDayView, objectConverter]}
|
|
||||||
></div>
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
import * as React from "bloatless-react";
|
|
||||||
|
|
||||||
import { Chat, MessageObject, MessageObjectWithIndex } from "../../Model/chatModel";
|
|
||||||
|
|
||||||
import { ObjectGridView } from "./objectGridView";
|
|
||||||
|
|
||||||
export function NoteObjectsView(
|
|
||||||
chat: Chat,
|
|
||||||
messageObjects: React.MapState<MessageObjectWithIndex>,
|
|
||||||
selectedObject: React.State<MessageObject | undefined>,
|
|
||||||
isShowingObjectModal: React.State<boolean>
|
|
||||||
) {
|
|
||||||
const notes = new React.ListState<MessageObjectWithIndex>();
|
|
||||||
messageObjects.handleAddition((messageObject) => {
|
|
||||||
const latest = chat.getMostRecentContent(messageObject);
|
|
||||||
if (!latest.noteContent) return;
|
|
||||||
|
|
||||||
notes.add(messageObject);
|
|
||||||
messageObjects.handleRemoval(messageObject, () =>
|
|
||||||
notes.remove(messageObject)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
return <div class="width-100 height-100 scroll-v padding">
|
|
||||||
{ObjectGridView(chat, notes, selectedObject, isShowingObjectModal)}
|
|
||||||
</div>;
|
|
||||||
}
|
|
||||||
@@ -1,256 +0,0 @@
|
|||||||
import * as React from "bloatless-react";
|
|
||||||
|
|
||||||
import { Chat, MessageObject } from "../../Model/chatModel";
|
|
||||||
import { usedObjectCategories, usedObjectStatuses } from "../../Model/model";
|
|
||||||
|
|
||||||
import { icons } from "../../icons";
|
|
||||||
import { stringToOptionTag } from "../../utility";
|
|
||||||
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)
|
|
||||||
);
|
|
||||||
|
|
||||||
// properties
|
|
||||||
const didEditContent = new React.State(false);
|
|
||||||
const editingNoteContent = React.createProxyState(
|
|
||||||
[selectedMessageObject],
|
|
||||||
() => selectedMessageObject.value.noteContent ?? ""
|
|
||||||
);
|
|
||||||
const editingCategory = React.createProxyState(
|
|
||||||
[selectedMessageObject],
|
|
||||||
() => selectedMessageObject.value.categoryName ?? ""
|
|
||||||
);
|
|
||||||
const editingStatus = React.createProxyState(
|
|
||||||
[selectedMessageObject],
|
|
||||||
() => selectedMessageObject.value.status ?? ""
|
|
||||||
);
|
|
||||||
const editingDate = React.createProxyState(
|
|
||||||
[selectedMessageObject],
|
|
||||||
() => selectedMessageObject.value.date ?? ""
|
|
||||||
);
|
|
||||||
const editingTime = React.createProxyState(
|
|
||||||
[selectedMessageObject],
|
|
||||||
() => selectedMessageObject.value.time ?? ""
|
|
||||||
);
|
|
||||||
const editingPriority = React.createProxyState(
|
|
||||||
[selectedMessageObject],
|
|
||||||
() => selectedMessageObject.value.priority ?? 0
|
|
||||||
);
|
|
||||||
React.bulkSubscribe(
|
|
||||||
[
|
|
||||||
editingNoteContent,
|
|
||||||
editingCategory,
|
|
||||||
editingStatus,
|
|
||||||
editingDate,
|
|
||||||
editingTime,
|
|
||||||
editingPriority,
|
|
||||||
],
|
|
||||||
() => (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,
|
|
||||||
priority: editingPriority.value,
|
|
||||||
categoryName: editingCategory.value,
|
|
||||||
status: editingStatus.value,
|
|
||||||
date: editingDate.value,
|
|
||||||
time: editingTime.value,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
chat.addObjectAndSend(messageObject);
|
|
||||||
closeModal();
|
|
||||||
}
|
|
||||||
|
|
||||||
function deleteAndClose() {
|
|
||||||
chat.deleteObject(messageObject);
|
|
||||||
closeModal();
|
|
||||||
}
|
|
||||||
|
|
||||||
const input = (
|
|
||||||
<input
|
|
||||||
bind:value={editingTitle}
|
|
||||||
placeholder={translation.objectTitlePlaceholder}
|
|
||||||
></input>
|
|
||||||
);
|
|
||||||
isPresented.subscribe(() => setTimeout(() => input.focus(), 100));
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div class="modal" toggle:open={isPresented} on:keydown={handleKeyDown}>
|
|
||||||
<div>
|
|
||||||
<main>
|
|
||||||
<h2>{chat.getObjectTitle(messageObject)}</h2>
|
|
||||||
<span class="secondary">{messageObject.id}</span>
|
|
||||||
|
|
||||||
<hr></hr>
|
|
||||||
|
|
||||||
<div class="flex-column gap">
|
|
||||||
<label class="tile">
|
|
||||||
<span class="icon">{icons.objectTitle}</span>
|
|
||||||
<div>
|
|
||||||
<span>{translation.objectTitle}</span>
|
|
||||||
{input}
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<hr></hr>
|
|
||||||
|
|
||||||
<label class="tile">
|
|
||||||
<span class="icon">{icons.objectHistory}</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>
|
|
||||||
|
|
||||||
<hr></hr>
|
|
||||||
|
|
||||||
<label class="tile">
|
|
||||||
<span class="icon">{icons.noteContent}</span>
|
|
||||||
<div>
|
|
||||||
<span>{translation.note}</span>
|
|
||||||
<textarea
|
|
||||||
rows="10"
|
|
||||||
class="height-auto"
|
|
||||||
bind:value={editingNoteContent}
|
|
||||||
placeholder={translation.noteContentPlaceholder}
|
|
||||||
></textarea>
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<hr></hr>
|
|
||||||
|
|
||||||
<label class="tile">
|
|
||||||
<span class="icon">{icons.categoryName}</span>
|
|
||||||
<div>
|
|
||||||
<span>{translation.category}</span>
|
|
||||||
<input
|
|
||||||
bind:value={editingCategory}
|
|
||||||
placeholder={translation.categoryPlaceholder}
|
|
||||||
list="object-categories"
|
|
||||||
></input>
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
<datalist
|
|
||||||
hidden
|
|
||||||
id="object-categories"
|
|
||||||
children:append={[usedObjectCategories, stringToOptionTag]}
|
|
||||||
></datalist>
|
|
||||||
|
|
||||||
<label class="tile">
|
|
||||||
<span class="icon">{icons.priority}</span>
|
|
||||||
<div>
|
|
||||||
<span>{translation.priority}</span>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
bind:value={editingPriority}
|
|
||||||
placeholder={translation.priorityPlaceholder}
|
|
||||||
></input>
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label class="tile">
|
|
||||||
<span class="icon">{icons.status}</span>
|
|
||||||
<div>
|
|
||||||
<span>{translation.status}</span>
|
|
||||||
<input
|
|
||||||
bind:value={editingStatus}
|
|
||||||
placeholder={translation.statusPlaceholder}
|
|
||||||
list="object-statuses"
|
|
||||||
></input>
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
<datalist
|
|
||||||
hidden
|
|
||||||
id="object-statuses"
|
|
||||||
children:append={[usedObjectStatuses, stringToOptionTag]}
|
|
||||||
></datalist>
|
|
||||||
|
|
||||||
<hr></hr>
|
|
||||||
|
|
||||||
<label class="tile">
|
|
||||||
<span class="icon">{icons.date}</span>
|
|
||||||
<div>
|
|
||||||
<span>{translation.date}</span>
|
|
||||||
<input type="date" bind:value={editingDate}></input>
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label class="tile">
|
|
||||||
<span class="icon">{icons.time}</span>
|
|
||||||
<div>
|
|
||||||
<span>{translation.time}</span>
|
|
||||||
<input type="time" bind:value={editingTime}></input>
|
|
||||||
</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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
import * as React from "bloatless-react";
|
|
||||||
|
|
||||||
import {
|
|
||||||
Chat,
|
|
||||||
MessageObject,
|
|
||||||
MessageObjectContent,
|
|
||||||
MessageObjectWithIndex,
|
|
||||||
} from "../../Model/chatModel";
|
|
||||||
|
|
||||||
import { icons } from "../../icons";
|
|
||||||
|
|
||||||
export function ObjectEntryView(
|
|
||||||
chat: Chat,
|
|
||||||
messageObject: MessageObjectWithIndex,
|
|
||||||
selectedObject: React.State<MessageObject | undefined>,
|
|
||||||
isShowingObjectModal: React.State<boolean>
|
|
||||||
) {
|
|
||||||
const latest = chat.getMostRecentContent(messageObject);
|
|
||||||
|
|
||||||
function select() {
|
|
||||||
selectedObject.value = messageObject;
|
|
||||||
isShowingObjectModal.value = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function dragStart(event: DragEvent) {
|
|
||||||
event.dataTransfer?.setData("text", messageObject.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
const fields = {
|
|
||||||
[icons.noteContent]: "---",
|
|
||||||
[icons.priority]: "---",
|
|
||||||
[icons.categoryName]: "---",
|
|
||||||
[icons.status]: "---",
|
|
||||||
[icons.date]: "---",
|
|
||||||
[icons.time]: "---",
|
|
||||||
};
|
|
||||||
Object.entries(latest).forEach((entry) => {
|
|
||||||
let [key, value] = entry as [keyof MessageObjectContent, string];
|
|
||||||
if (!value) return;
|
|
||||||
|
|
||||||
const icon = icons[key];
|
|
||||||
if (!icon) return;
|
|
||||||
|
|
||||||
if (!fields[icon]) return;
|
|
||||||
fields[icon] = value;
|
|
||||||
});
|
|
||||||
|
|
||||||
const fieldElements = Object.entries(fields).map((field) => {
|
|
||||||
const [icon, value] = field;
|
|
||||||
return (
|
|
||||||
<span class="flex-row control-gap flex align-center padding-right ellipsis">
|
|
||||||
<span class="icon">{icon}</span>
|
|
||||||
<span class="ellipsis">{value}</span>
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const view = (
|
|
||||||
<button
|
|
||||||
class="tile flex-no"
|
|
||||||
on:click={select}
|
|
||||||
draggable="true"
|
|
||||||
on:dragstart={dragStart}
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<b class="ellipsis">{chat.getObjectTitle(messageObject)}</b>
|
|
||||||
<hr></hr>
|
|
||||||
<span
|
|
||||||
class="grid height-100 flex secondary"
|
|
||||||
style="grid-template-columns: 1fr 1fr; grid-template-rows: repeat(3, 1.7rem)"
|
|
||||||
>
|
|
||||||
{...fieldElements}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
messageObject.index.subscribe(newIndex => {
|
|
||||||
view.style.order = newIndex;
|
|
||||||
})
|
|
||||||
return view;
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
import * as React from "bloatless-react";
|
|
||||||
|
|
||||||
import { Chat, MessageObject, MessageObjectWithIndex } from "../../Model/chatModel";
|
|
||||||
|
|
||||||
import { ObjectEntryView } from "./objectEntryView";
|
|
||||||
|
|
||||||
export function ObjectGridView(
|
|
||||||
chat: Chat,
|
|
||||||
messageObjects:
|
|
||||||
| React.ListState<MessageObjectWithIndex>
|
|
||||||
| React.MapState<MessageObjectWithIndex>,
|
|
||||||
selectedObject: React.State<MessageObject | undefined>,
|
|
||||||
isShowingObjectModal: React.State<boolean>
|
|
||||||
) {
|
|
||||||
const objectConverter: React.StateItemConverter<MessageObjectWithIndex> = (
|
|
||||||
messageObject
|
|
||||||
) => {
|
|
||||||
return ObjectEntryView(
|
|
||||||
chat,
|
|
||||||
messageObject,
|
|
||||||
selectedObject,
|
|
||||||
isShowingObjectModal
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
class="grid gap"
|
|
||||||
style="grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));"
|
|
||||||
children:prepend={[messageObjects, objectConverter]}
|
|
||||||
></div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,296 +0,0 @@
|
|||||||
import * as React from "bloatless-react";
|
|
||||||
|
|
||||||
import {
|
|
||||||
Chat,
|
|
||||||
MessageObject,
|
|
||||||
MessageObjectWithIndex,
|
|
||||||
} from "../../Model/chatModel";
|
|
||||||
import { getRawObjectIndex, stringToOptionTag } from "../../utility";
|
|
||||||
import { objectFilterInput, previousObjectSearches } from "../../Model/model";
|
|
||||||
|
|
||||||
import { AllObjectsView } from "./allObjectsView";
|
|
||||||
import { CalendarView } from "./calendarView";
|
|
||||||
import { KanbanView } from "./kanbanView";
|
|
||||||
import { NoteObjectsView } from "./noteObjectsView";
|
|
||||||
import { ObjectDetailModal } from "./objectDetailModal";
|
|
||||||
import { ObjectGridView } from "./objectGridView";
|
|
||||||
import { StatusView } from "./statusView";
|
|
||||||
import { icons } from "../../icons";
|
|
||||||
import { translation } from "../../translations";
|
|
||||||
|
|
||||||
export const viewTypes = {
|
|
||||||
all: [translation.viewAll, "grid_view"],
|
|
||||||
notes: [translation.viewNotes, icons.noteContent],
|
|
||||||
calendar: [translation.viewCalendar, icons.date],
|
|
||||||
kanban: [translation.viewKanban, "view_kanban"],
|
|
||||||
status: [translation.viewStatus, icons.status],
|
|
||||||
};
|
|
||||||
|
|
||||||
export function ObjectPane(chat: Chat) {
|
|
||||||
// selection and modal
|
|
||||||
const isShowingObjectModal = new React.State(false);
|
|
||||||
const isShowingFilterModel = new React.State(false);
|
|
||||||
|
|
||||||
const selectedObject = new React.State<MessageObject | undefined>(undefined);
|
|
||||||
|
|
||||||
// methods
|
|
||||||
function createObject() {
|
|
||||||
const newObject = chat.createObjectFromTitle("");
|
|
||||||
chat.addObjectAndSend(newObject);
|
|
||||||
selectedObject.value = newObject;
|
|
||||||
isShowingObjectModal.value = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function showFilters() {
|
|
||||||
isShowingFilterModel.value = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function closeFilters() {
|
|
||||||
isShowingFilterModel.value = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// filter
|
|
||||||
const appliedFilter = new React.State("");
|
|
||||||
const resultCount = new React.State(0);
|
|
||||||
const resultText = React.createProxyState([appliedFilter, resultCount], () =>
|
|
||||||
translation.searchTitleText(appliedFilter.value, resultCount.value)
|
|
||||||
);
|
|
||||||
const visibleMessageObjects = new React.MapState<MessageObjectWithIndex>();
|
|
||||||
const isFilterEmpty = React.createProxyState(
|
|
||||||
[appliedFilter],
|
|
||||||
() => appliedFilter.value == ""
|
|
||||||
);
|
|
||||||
|
|
||||||
function resetFilter() {
|
|
||||||
objectFilterInput.value = "";
|
|
||||||
applyFilter();
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyFilter() {
|
|
||||||
appliedFilter.value = objectFilterInput.value;
|
|
||||||
if (!previousObjectSearches.value.has(appliedFilter.value))
|
|
||||||
previousObjectSearches.add(appliedFilter.value);
|
|
||||||
|
|
||||||
const allObjects = [...chat.objects.value.values()];
|
|
||||||
allObjects.forEach((messageObject, i) => {
|
|
||||||
const doesMatch = checkIfMatchesFilter(messageObject);
|
|
||||||
if (doesMatch) {
|
|
||||||
visibleMessageObjects.set(messageObject.id, new MessageObjectWithIndex(messageObject));
|
|
||||||
} else {
|
|
||||||
visibleMessageObjects.remove(messageObject.id);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
resultCount.value = visibleMessageObjects.value.size;
|
|
||||||
}
|
|
||||||
|
|
||||||
chat.objects.handleAddition((messageObject) => {
|
|
||||||
if (checkIfMatchesFilter(messageObject) == false) return;
|
|
||||||
visibleMessageObjects.set(messageObject.id, new MessageObjectWithIndex(messageObject));
|
|
||||||
chat.objects.handleRemoval(messageObject, () => {
|
|
||||||
visibleMessageObjects.remove(messageObject.id);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
function checkIfMatchesFilter(messageObject: MessageObject) {
|
|
||||||
if (isFilterEmpty.value == true) return true;
|
|
||||||
|
|
||||||
const stringsInObject: string[] = [];
|
|
||||||
const objectTitle = messageObject.title || translation.untitledObject;
|
|
||||||
stringsInObject.push(objectTitle);
|
|
||||||
|
|
||||||
const latest = chat.getMostRecentContent(messageObject);
|
|
||||||
if (latest) {
|
|
||||||
Object.values(latest).forEach((value) => {
|
|
||||||
if (typeof value != "string") return;
|
|
||||||
stringsInObject.push(value);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const wordsOfObject = stringsInObject
|
|
||||||
.map((string) => string.toLowerCase().split(" "))
|
|
||||||
.flat();
|
|
||||||
|
|
||||||
const wordsOfSearchTerm = appliedFilter.value.toLowerCase().split(" ");
|
|
||||||
for (const word of wordsOfSearchTerm) {
|
|
||||||
if (word[0] == "-") {
|
|
||||||
// exclusion
|
|
||||||
const wordContent = word.substring(1);
|
|
||||||
if (wordsOfObject.includes(wordContent)) return false;
|
|
||||||
} else {
|
|
||||||
return wordsOfObject.includes(word);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
applyFilter();
|
|
||||||
|
|
||||||
// sorting
|
|
||||||
visibleMessageObjects.subscribe((visibleMessageObjects) => {
|
|
||||||
let indices = [...visibleMessageObjects.values()]
|
|
||||||
.map((messageObject) => {
|
|
||||||
const latest = chat.getMostRecentContent(messageObject);
|
|
||||||
return getRawObjectIndex(latest);
|
|
||||||
})
|
|
||||||
.sort();
|
|
||||||
|
|
||||||
visibleMessageObjects.forEach((messageObject) => {
|
|
||||||
const latest = chat.getMostRecentContent(messageObject);
|
|
||||||
messageObject.index.value = indices.indexOf(getRawObjectIndex(latest));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// view
|
|
||||||
const objectModal = React.createProxyState(
|
|
||||||
[chat.objects, selectedObject],
|
|
||||||
() => {
|
|
||||||
if (selectedObject.value == undefined) return <div></div>;
|
|
||||||
|
|
||||||
return ObjectDetailModal(
|
|
||||||
chat,
|
|
||||||
selectedObject.value,
|
|
||||||
isShowingObjectModal
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const mainView = React.createProxyState([chat.viewType], () => {
|
|
||||||
function getViewFunction() {
|
|
||||||
switch (chat.viewType.value) {
|
|
||||||
case "notes":
|
|
||||||
return NoteObjectsView;
|
|
||||||
case "kanban":
|
|
||||||
return KanbanView;
|
|
||||||
case "status":
|
|
||||||
return StatusView;
|
|
||||||
case "calendar":
|
|
||||||
return CalendarView;
|
|
||||||
default:
|
|
||||||
return AllObjectsView;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return getViewFunction()(
|
|
||||||
chat,
|
|
||||||
visibleMessageObjects,
|
|
||||||
selectedObject,
|
|
||||||
isShowingObjectModal
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div class="chat-object-view flex-column scroll-no padding-0">
|
|
||||||
<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, chat.viewType)
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
class="height-100"
|
|
||||||
aria-label={translation.filterObjects}
|
|
||||||
on:click={showFilters}
|
|
||||||
>
|
|
||||||
<span class="icon">filter_list</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="object-content width-100 height-100 flex scroll-no"
|
|
||||||
children:set={mainView}
|
|
||||||
></div>
|
|
||||||
|
|
||||||
<div class="modal" toggle:open={isShowingFilterModel}>
|
|
||||||
<div style="max-width: 2084px">
|
|
||||||
<main class="gap">
|
|
||||||
<h2>{translation.filterObjects}</h2>
|
|
||||||
|
|
||||||
<div class="flex-column">
|
|
||||||
<label class="tile">
|
|
||||||
<span class="icon">search</span>
|
|
||||||
<div>
|
|
||||||
<span>{translation.searchTitle}</span>
|
|
||||||
<input
|
|
||||||
bind:value={objectFilterInput}
|
|
||||||
on:enter={applyFilter}
|
|
||||||
placeholder={translation.searchPlaceholder}
|
|
||||||
list="object-searches"
|
|
||||||
></input>
|
|
||||||
<datalist
|
|
||||||
hidden
|
|
||||||
id="object-searches"
|
|
||||||
children:append={[
|
|
||||||
previousObjectSearches,
|
|
||||||
stringToOptionTag,
|
|
||||||
]}
|
|
||||||
></datalist>
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="flex-row width-input">
|
|
||||||
<button
|
|
||||||
class="width-50 flex"
|
|
||||||
on:click={resetFilter}
|
|
||||||
toggle:disabled={isFilterEmpty}
|
|
||||||
>
|
|
||||||
{translation.reset}
|
|
||||||
</button>
|
|
||||||
<button class="width-50 flex primary" on:click={applyFilter}>
|
|
||||||
{translation.search}
|
|
||||||
<span class="icon">arrow_forward</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<hr></hr>
|
|
||||||
|
|
||||||
<b class="secondary" subscribe:innerText={resultText}></b>
|
|
||||||
|
|
||||||
{ObjectGridView(
|
|
||||||
chat,
|
|
||||||
visibleMessageObjects,
|
|
||||||
selectedObject,
|
|
||||||
isShowingObjectModal
|
|
||||||
)}
|
|
||||||
</main>
|
|
||||||
<button on:click={closeFilters}>
|
|
||||||
{translation.close}
|
|
||||||
<span class="icon">close</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import * as React from "bloatless-react";
|
|
||||||
|
|
||||||
import { translation } from "../../translations";
|
|
||||||
|
|
||||||
export function PlaceholderView() {
|
|
||||||
return <div class="flex-column width-100 height-100 align-center justify-center secondary">
|
|
||||||
{translation.noObjects}
|
|
||||||
</div>;
|
|
||||||
}
|
|
||||||
@@ -1,258 +0,0 @@
|
|||||||
import * as React from "bloatless-react";
|
|
||||||
|
|
||||||
import { Chat, MessageObject, MessageObjectWithIndex } from "../../Model/chatModel";
|
|
||||||
|
|
||||||
import { ObjectEntryView } from "./objectEntryView";
|
|
||||||
import { RenameView } from "../renameView";
|
|
||||||
|
|
||||||
interface CategoryRow {
|
|
||||||
category: string;
|
|
||||||
statusColumns: React.MapState<StatusColumn>;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface StatusColumn {
|
|
||||||
status: string;
|
|
||||||
items: React.ListState<StatusCellItem>;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface StatusCellItem {
|
|
||||||
priority: number;
|
|
||||||
status: string;
|
|
||||||
messageObject: MessageObjectWithIndex;
|
|
||||||
}
|
|
||||||
|
|
||||||
// data only
|
|
||||||
interface StatusData {
|
|
||||||
title: string;
|
|
||||||
items: MessageObject[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function StatusView(
|
|
||||||
chat: Chat,
|
|
||||||
messageObjects: React.MapState<MessageObjectWithIndex>,
|
|
||||||
selectedObject: React.State<MessageObject | undefined>,
|
|
||||||
isShowingObjectModal: React.State<boolean>
|
|
||||||
) {
|
|
||||||
const rows = new React.MapState<CategoryRow>();
|
|
||||||
const statuses = new React.MapState<StatusData>();
|
|
||||||
const sortedStatuses = React.createProxyState([statuses], () =>
|
|
||||||
[...statuses.value.values()]
|
|
||||||
.map((statusData) => statusData.title)
|
|
||||||
.sort((a, b) => a.localeCompare(b))
|
|
||||||
);
|
|
||||||
const sortedCategoryNames = React.createProxyState([rows], () =>
|
|
||||||
[...rows.value.values()]
|
|
||||||
.map((row) => row.category)
|
|
||||||
.sort((a, b) => a.localeCompare(b))
|
|
||||||
);
|
|
||||||
|
|
||||||
messageObjects.subscribe(() => {
|
|
||||||
rows.clear();
|
|
||||||
statuses.clear();
|
|
||||||
|
|
||||||
// add items
|
|
||||||
messageObjects.value.forEach((messageObject) => {
|
|
||||||
const latest = chat.getMostRecentContent(messageObject);
|
|
||||||
if (!latest.categoryName || !latest.status) return;
|
|
||||||
|
|
||||||
const category = latest.categoryName;
|
|
||||||
const status = latest.status;
|
|
||||||
|
|
||||||
if (!statuses.value.has(status)) {
|
|
||||||
statuses.set(status, { title: status, items: [] });
|
|
||||||
}
|
|
||||||
statuses.value.get(status)!.items.push(messageObject);
|
|
||||||
|
|
||||||
if (!rows.value.has(category)) {
|
|
||||||
rows.set(category, {
|
|
||||||
category: category,
|
|
||||||
statusColumns: new React.MapState(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const row = rows.value.get(category)!;
|
|
||||||
|
|
||||||
if (!row.statusColumns.value.has(status)) {
|
|
||||||
row.statusColumns.set(status, {
|
|
||||||
status: status,
|
|
||||||
items: new React.ListState(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const column = row.statusColumns.value.get(status)!;
|
|
||||||
|
|
||||||
column.items.add({
|
|
||||||
priority: latest.priority ?? 0,
|
|
||||||
status,
|
|
||||||
messageObject,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// add status placeholders
|
|
||||||
statuses.value.forEach((statusItemList) => {
|
|
||||||
rows.value.forEach((row) => {
|
|
||||||
if (row.statusColumns.value.has(statusItemList.title)) return;
|
|
||||||
row.statusColumns.set(statusItemList.title, {
|
|
||||||
status: statusItemList.title,
|
|
||||||
items: new React.ListState(),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const statusDataToHeaderView: React.StateItemConverter<StatusData> = (
|
|
||||||
statusData
|
|
||||||
) => {
|
|
||||||
const view = StatusHeaderView(chat, statusData);
|
|
||||||
sortedStatuses.subscribe(
|
|
||||||
() => (view.style.order = sortedStatuses.value.indexOf(statusData.title))
|
|
||||||
);
|
|
||||||
return view;
|
|
||||||
};
|
|
||||||
|
|
||||||
const categoryRowToView: React.StateItemConverter<CategoryRow> = (row) => {
|
|
||||||
const view = KanbanRowView(
|
|
||||||
chat,
|
|
||||||
row,
|
|
||||||
sortedStatuses,
|
|
||||||
selectedObject,
|
|
||||||
isShowingObjectModal
|
|
||||||
);
|
|
||||||
sortedCategoryNames.subscribe(
|
|
||||||
() => (view.style.order = sortedCategoryNames.value.indexOf(row.category))
|
|
||||||
);
|
|
||||||
return view;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div class="flex-column large-gap width-100 height-100 scroll-v scroll-h padding">
|
|
||||||
<div class="flex-row large-gap">
|
|
||||||
<div class="flex-column object-entry-wide"></div>
|
|
||||||
<div
|
|
||||||
class="flex-row large-gap"
|
|
||||||
children:append={[statuses, statusDataToHeaderView]}
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="flex-column large-gap"
|
|
||||||
children:append={[rows, categoryRowToView]}
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function StatusHeaderView(chat: Chat, status: StatusData) {
|
|
||||||
const editingStatus = new React.State(status.title);
|
|
||||||
|
|
||||||
function rename() {
|
|
||||||
status.items.forEach((messageObject) => {
|
|
||||||
const latest = chat.getMostRecentContent(messageObject);
|
|
||||||
|
|
||||||
latest.status = editingStatus.value;
|
|
||||||
chat.addObject(messageObject);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div class="object-entry-wide">
|
|
||||||
{RenameView(editingStatus, status.title, rename)}
|
|
||||||
<hr></hr>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function KanbanRowView(
|
|
||||||
chat: Chat,
|
|
||||||
kanbanRow: CategoryRow,
|
|
||||||
sortedStatuses: React.State<string[]>,
|
|
||||||
selectedObject: React.State<MessageObject | undefined>,
|
|
||||||
isShowingObjectModal: React.State<boolean>
|
|
||||||
) {
|
|
||||||
const editingCategory = new React.State(kanbanRow.category);
|
|
||||||
|
|
||||||
function renameCategory() {
|
|
||||||
kanbanRow.statusColumns.value.forEach((statusColumns) => {
|
|
||||||
statusColumns.items.value.forEach((statusCellItem) => {
|
|
||||||
const { messageObject } = statusCellItem;
|
|
||||||
const latest = chat.getMostRecentContent(messageObject);
|
|
||||||
latest.categoryName = editingCategory.value;
|
|
||||||
chat.addObjectAndSend(messageObject);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const statusColumnToEntryView: React.StateItemConverter<StatusColumn> = (
|
|
||||||
statusColumn
|
|
||||||
) => {
|
|
||||||
const view = StatusColumnView(
|
|
||||||
chat,
|
|
||||||
statusColumn.items,
|
|
||||||
kanbanRow.category,
|
|
||||||
statusColumn.status,
|
|
||||||
selectedObject,
|
|
||||||
isShowingObjectModal
|
|
||||||
);
|
|
||||||
sortedStatuses.subscribe(
|
|
||||||
() =>
|
|
||||||
(view.style.order = sortedStatuses.value.indexOf(statusColumn.status))
|
|
||||||
);
|
|
||||||
return view;
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<div class="flex-row flex-no large-gap">
|
|
||||||
<div class="flex-row align-start">
|
|
||||||
{RenameView(editingCategory, kanbanRow.category, renameCategory)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="flex-row large-gap"
|
|
||||||
children:append={[kanbanRow.statusColumns, statusColumnToEntryView]}
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function StatusColumnView(
|
|
||||||
chat: Chat,
|
|
||||||
items: React.ListState<StatusCellItem>,
|
|
||||||
category: string,
|
|
||||||
status: string,
|
|
||||||
selectedObject: React.State<MessageObject | undefined>,
|
|
||||||
isShowingObjectModal: React.State<boolean>
|
|
||||||
) {
|
|
||||||
function dragOver(event: DragEvent) {
|
|
||||||
event.preventDefault();
|
|
||||||
}
|
|
||||||
|
|
||||||
function drop(event: DragEvent) {
|
|
||||||
event.preventDefault();
|
|
||||||
var id = event.dataTransfer?.getData("text");
|
|
||||||
if (!id) return;
|
|
||||||
|
|
||||||
const messageObject = chat.objects.value.get(id);
|
|
||||||
if (!messageObject) return;
|
|
||||||
|
|
||||||
const newContent = chat.createObjectContent();
|
|
||||||
newContent.categoryName = category;
|
|
||||||
newContent.status = status;
|
|
||||||
chat.updateObjectContent(messageObject, newContent);
|
|
||||||
chat.addObjectAndSend(messageObject);
|
|
||||||
}
|
|
||||||
|
|
||||||
const cellItemToEntryView: React.StateItemConverter<StatusCellItem> = (
|
|
||||||
cellItem
|
|
||||||
) =>
|
|
||||||
ObjectEntryView(
|
|
||||||
chat,
|
|
||||||
cellItem.messageObject,
|
|
||||||
selectedObject,
|
|
||||||
isShowingObjectModal
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
class="flex-column gap object-entry-wide"
|
|
||||||
on:dragover={dragOver}
|
|
||||||
on:drop={drop}
|
|
||||||
children:append={[items, cellItemToEntryView]}
|
|
||||||
></div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
import * as React from "bloatless-react";
|
|
||||||
|
|
||||||
import {
|
|
||||||
cannotCreateChat,
|
|
||||||
chats,
|
|
||||||
createChat,
|
|
||||||
newChatName,
|
|
||||||
selectChat,
|
|
||||||
selectedChat,
|
|
||||||
} from "../../Model/model";
|
|
||||||
|
|
||||||
import { Chat } from "../../Model/chatModel";
|
|
||||||
import { translation } from "../../translations";
|
|
||||||
|
|
||||||
const chatConverter: React.StateItemConverter<Chat> = (chat) => {
|
|
||||||
function select() {
|
|
||||||
selectChat(chat);
|
|
||||||
}
|
|
||||||
|
|
||||||
const isSelected = React.createProxyState(
|
|
||||||
[selectedChat],
|
|
||||||
() => selectedChat.value == chat
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
class="tile"
|
|
||||||
on:click={select}
|
|
||||||
toggle:selected={isSelected}
|
|
||||||
toggle:unread={chat.hasUnreadMessages}
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<b class="ellipsis" subscribe:innerText={chat.primaryChannel}></b>
|
|
||||||
</div>
|
|
||||||
<span class="icon">arrow_forward</span>
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export function ChatListSection() {
|
|
||||||
return (
|
|
||||||
<div class="flex-column">
|
|
||||||
<h2>{translation.chats}</h2>
|
|
||||||
|
|
||||||
<label class="tile">
|
|
||||||
<span class="icon">forum</span>
|
|
||||||
<div>
|
|
||||||
<span>{translation.primaryChannel}</span>
|
|
||||||
<input
|
|
||||||
bind:value={newChatName}
|
|
||||||
placeholder={translation.primaryChannelPlaceholder}
|
|
||||||
on:enter={createChat}
|
|
||||||
></input>
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
<div class="flex-row justify-end">
|
|
||||||
<button
|
|
||||||
class="primary width-50"
|
|
||||||
toggle:disabled={cannotCreateChat}
|
|
||||||
on:click={createChat}
|
|
||||||
>
|
|
||||||
{translation.addChat}
|
|
||||||
<span class="icon">add</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<hr></hr>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="flex-column gap"
|
|
||||||
children:append={[chats, chatConverter]}
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,131 +0,0 @@
|
|||||||
import * as React from "bloatless-react";
|
|
||||||
|
|
||||||
import {
|
|
||||||
cannotConnect,
|
|
||||||
cannotDeleteMailbox,
|
|
||||||
cannotDisonnect,
|
|
||||||
cannotRequestMailbox,
|
|
||||||
cannotResetAddress,
|
|
||||||
connect,
|
|
||||||
deleteMailbox,
|
|
||||||
disconnect,
|
|
||||||
outboxText,
|
|
||||||
outboxTextStyle,
|
|
||||||
previousAddresses,
|
|
||||||
requestMailbox,
|
|
||||||
resetAddressInput,
|
|
||||||
serverAddressInput,
|
|
||||||
} from "../../Model/model";
|
|
||||||
|
|
||||||
import { stringToOptionTag } from "../../utility";
|
|
||||||
import { translation } from "../../translations";
|
|
||||||
|
|
||||||
export function ConnectionSection() {
|
|
||||||
const stringToPreviouAddressOption: React.StateItemConverter<string> = (
|
|
||||||
value
|
|
||||||
) => {
|
|
||||||
const isSelected = value == serverAddressInput.value;
|
|
||||||
return <option toggle:selected={isSelected}>{value}</option>;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div class="flex-column">
|
|
||||||
<h2>{translation.connection}</h2>
|
|
||||||
|
|
||||||
<label class="tile">
|
|
||||||
<span class="icon">cell_tower</span>
|
|
||||||
<div>
|
|
||||||
<span>{translation.serverAddress}</span>
|
|
||||||
<input
|
|
||||||
bind:value={serverAddressInput}
|
|
||||||
on:enter={connect}
|
|
||||||
placeholder={translation.serverAddressPlaceholder}
|
|
||||||
list="previous-addresses"
|
|
||||||
></input>
|
|
||||||
<datalist
|
|
||||||
hidden
|
|
||||||
id="previous-addresses"
|
|
||||||
children:append={[previousAddresses, stringToOptionTag]}
|
|
||||||
></datalist>
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
<label class="tile">
|
|
||||||
<span class="icon">history</span>
|
|
||||||
<div>
|
|
||||||
<span>{translation.previousConnections}</span>
|
|
||||||
<select
|
|
||||||
bind:value={serverAddressInput}
|
|
||||||
children:append={[previousAddresses, stringToPreviouAddressOption]}
|
|
||||||
></select>
|
|
||||||
<span class="icon">arrow_drop_down</span>
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
<div class="flex-row width-input justify-end">
|
|
||||||
<button
|
|
||||||
class="danger width-100 flex-1 justify-center"
|
|
||||||
aria-label={translation.disconnect}
|
|
||||||
on:click={disconnect}
|
|
||||||
toggle:disabled={cannotDisonnect}
|
|
||||||
>
|
|
||||||
<span class="icon">close</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="width-100 flex-1 justify-center"
|
|
||||||
aria-label={translation.undoChanges}
|
|
||||||
on:click={resetAddressInput}
|
|
||||||
toggle:disabled={cannotResetAddress}
|
|
||||||
>
|
|
||||||
<span class="icon">undo</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="primary width-100 flex-1 justify-center"
|
|
||||||
aria-label={translation.connectToServer}
|
|
||||||
on:click={connect}
|
|
||||||
toggle:disabled={cannotConnect}
|
|
||||||
>
|
|
||||||
<span class="icon">arrow_forward</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<hr></hr>
|
|
||||||
|
|
||||||
<div class="tile">
|
|
||||||
<span class="icon">inbox</span>
|
|
||||||
<div>
|
|
||||||
<b>{translation.mailbox}</b>
|
|
||||||
<span class="secondary">{translation.mailboxExplanation}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex-row width-input justify-end">
|
|
||||||
<button
|
|
||||||
class="danger width-100 flex-1"
|
|
||||||
on:click={deleteMailbox}
|
|
||||||
toggle:disabled={cannotDeleteMailbox}
|
|
||||||
>
|
|
||||||
{translation.deleteMailbox}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="primary width-100 flex-1"
|
|
||||||
on:click={requestMailbox}
|
|
||||||
toggle:disabled={cannotRequestMailbox}
|
|
||||||
>
|
|
||||||
{translation.requestMailbox}
|
|
||||||
<span class="icon">arrow_forward</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<hr></hr>
|
|
||||||
|
|
||||||
<div class="tile">
|
|
||||||
<span class="icon">outbox</span>
|
|
||||||
<div>
|
|
||||||
<b>{translation.outbox}</b>
|
|
||||||
<span
|
|
||||||
set:class={outboxTextStyle}
|
|
||||||
subscribe:innerText={outboxText}
|
|
||||||
></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
import * as React from "bloatless-react";
|
|
||||||
|
|
||||||
import { senderName } from "../../Model/model";
|
|
||||||
import { translation } from "../../translations";
|
|
||||||
|
|
||||||
export function PersonalSection() {
|
|
||||||
return (
|
|
||||||
<div class="flex-column">
|
|
||||||
<label class="tile">
|
|
||||||
<span class="icon">account_circle</span>
|
|
||||||
<div>
|
|
||||||
<span>{translation.yourName}</span>
|
|
||||||
<input
|
|
||||||
bind:value={senderName}
|
|
||||||
placeholder={translation.namePlaceholder}
|
|
||||||
></input>
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
import * as React from "bloatless-react";
|
|
||||||
|
|
||||||
import { translation } from "../translations";
|
|
||||||
|
|
||||||
export function RenameView(
|
|
||||||
editingName: React.State<string>,
|
|
||||||
initialName: string,
|
|
||||||
rename: () => void
|
|
||||||
) {
|
|
||||||
const cannotRename = React.createProxyState(
|
|
||||||
[editingName],
|
|
||||||
() => editingName.value == initialName || editingName.value == ""
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div class="flex-row align-center justify-apart object-entry-wide">
|
|
||||||
<input bind:value={editingName} on:enter={rename}></input>
|
|
||||||
<button
|
|
||||||
class="primary"
|
|
||||||
aria-label={translation.rename}
|
|
||||||
on:click={rename}
|
|
||||||
toggle:disabled={cannotRename}
|
|
||||||
>
|
|
||||||
<span class="icon">edit</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
import * as React from "bloatless-react";
|
|
||||||
|
|
||||||
import {
|
|
||||||
clearAddresses,
|
|
||||||
clearCategories,
|
|
||||||
clearObjectSearches,
|
|
||||||
clearStatuses,
|
|
||||||
firstDayOfWeek,
|
|
||||||
isPresentingSettingsModal,
|
|
||||||
repairApp,
|
|
||||||
toggleSettings,
|
|
||||||
zoomIn,
|
|
||||||
zoomOut,
|
|
||||||
} from "../Model/model";
|
|
||||||
|
|
||||||
import { icons } from "../icons";
|
|
||||||
import { translation } from "../translations";
|
|
||||||
|
|
||||||
export function SettingsModal() {
|
|
||||||
const stringToWeekdayOption: React.StateItemConverter<number> = (value) => {
|
|
||||||
const isSelected = value == firstDayOfWeek.value;
|
|
||||||
return <option toggle:selected={isSelected} value={value}>{translation.weekdays[value]}</option>;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div class="modal" toggle:open={isPresentingSettingsModal}>
|
|
||||||
<div>
|
|
||||||
<main>
|
|
||||||
<h2>{translation.settings}</h2>
|
|
||||||
|
|
||||||
<label class="tile">
|
|
||||||
<span class="icon">calendar_month</span>
|
|
||||||
<div>
|
|
||||||
<span>{translation.firstDayOfWeek}</span>
|
|
||||||
<select bind:value={firstDayOfWeek}>
|
|
||||||
{...translation.weekdays.map((weekday, i) =>
|
|
||||||
stringToWeekdayOption(i)
|
|
||||||
)}
|
|
||||||
</select>
|
|
||||||
<span class="icon">arrow_drop_down</span>
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<hr></hr>
|
|
||||||
|
|
||||||
<div class="flex-row width-input gap">
|
|
||||||
<button class="width-100 flex justify-start" on:click={zoomOut}>
|
|
||||||
<span class="icon">zoom_out</span>
|
|
||||||
{translation.zoomOut}
|
|
||||||
</button>
|
|
||||||
<button class="width-100 flex justify-start" on:click={zoomIn}>
|
|
||||||
<span class="icon">zoom_in</span>
|
|
||||||
{translation.zoomIn}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<hr></hr>
|
|
||||||
|
|
||||||
<div class="flex-column width-input">
|
|
||||||
<button class="tile width-100 flex-1" on:click={repairApp}>
|
|
||||||
<span class="icon">handyman</span>
|
|
||||||
{translation.repairApp}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<hr></hr>
|
|
||||||
|
|
||||||
<div class="flex-column width-input gap">
|
|
||||||
<button class="tile width-100 flex-1" on:click={clearAddresses}>
|
|
||||||
<span class="icon">cell_tower</span>
|
|
||||||
{translation.clearAddresses}
|
|
||||||
</button>
|
|
||||||
<button class="tile width-100 flex-1" on:click={clearCategories}>
|
|
||||||
<span class="icon">{icons.categoryName}</span>
|
|
||||||
{translation.clearCategories}
|
|
||||||
</button>
|
|
||||||
<button class="tile width-100 flex-1" on:click={clearStatuses}>
|
|
||||||
<span class="icon">{icons.status}</span>
|
|
||||||
{translation.clearStatuses}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="tile width-100 flex-1"
|
|
||||||
on:click={clearObjectSearches}
|
|
||||||
>
|
|
||||||
<span class="icon">search</span>
|
|
||||||
{translation.clearObjectFilters}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
<button class="width-100" on:click={toggleSettings}>
|
|
||||||
{translation.close}
|
|
||||||
<span class="icon">close</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,115 +0,0 @@
|
|||||||
const IV_SIZE = 12;
|
|
||||||
const ENCRYPTION_ALG = "AES-GCM";
|
|
||||||
|
|
||||||
// minimal
|
|
||||||
export async function encryptString(
|
|
||||||
plaintext: string,
|
|
||||||
passphrase: string
|
|
||||||
): Promise<string> {
|
|
||||||
if (!window.crypto.subtle) return plaintext;
|
|
||||||
|
|
||||||
const iv = generateIV();
|
|
||||||
const key = await importKey(passphrase, "encrypt");
|
|
||||||
const encryptedArray = await encrypt(iv, key, plaintext);
|
|
||||||
|
|
||||||
const encryptionData = {
|
|
||||||
iv: uInt8ToArray(iv),
|
|
||||||
encryptedArray: uInt8ToArray(encryptedArray),
|
|
||||||
};
|
|
||||||
return btoa(JSON.stringify(encryptionData));
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function decryptString(
|
|
||||||
cyphertext: string,
|
|
||||||
passphrase: string
|
|
||||||
): Promise<string> {
|
|
||||||
try {
|
|
||||||
const encrypionData = JSON.parse(atob(cyphertext));
|
|
||||||
const iv = arrayToUint8(encrypionData.iv);
|
|
||||||
const encryptedArray = arrayToUint8(encrypionData.encryptedArray);
|
|
||||||
|
|
||||||
const key = await importKey(passphrase, "decrypt");
|
|
||||||
return await decrypt(iv, key, encryptedArray);
|
|
||||||
} catch {
|
|
||||||
return cyphertext;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// process
|
|
||||||
export function encode(string: string): Uint8Array {
|
|
||||||
return new TextEncoder().encode(string);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function decode(array: Uint8Array): string {
|
|
||||||
return new TextDecoder("utf-8").decode(array);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function encrypt(
|
|
||||||
iv: Uint8Array,
|
|
||||||
key: CryptoKey,
|
|
||||||
message: string
|
|
||||||
): Promise<Uint8Array> {
|
|
||||||
const arrayBuffer = await window.crypto.subtle.encrypt(
|
|
||||||
{ name: ENCRYPTION_ALG, iv },
|
|
||||||
key,
|
|
||||||
encode(message)
|
|
||||||
);
|
|
||||||
return new Uint8Array(arrayBuffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function decrypt(
|
|
||||||
iv: Uint8Array,
|
|
||||||
key: CryptoKey,
|
|
||||||
cyphertext: Uint8Array
|
|
||||||
): Promise<string> {
|
|
||||||
const arrayBuffer = await crypto.subtle.decrypt(
|
|
||||||
{ name: ENCRYPTION_ALG, iv },
|
|
||||||
key,
|
|
||||||
cyphertext
|
|
||||||
);
|
|
||||||
return arrayBufferToString(arrayBuffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function hash(encoded: Uint8Array): Promise<ArrayBuffer> {
|
|
||||||
return await crypto.subtle.digest("SHA-256", encoded);
|
|
||||||
}
|
|
||||||
|
|
||||||
// iv
|
|
||||||
export function generateIV(): Uint8Array {
|
|
||||||
return crypto.getRandomValues(new Uint8Array(IV_SIZE));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function splitMergedString(mergedString: string): [Uint8Array, string] {
|
|
||||||
const iv = mergedString.slice(0, IV_SIZE);
|
|
||||||
const encryptedString = mergedString.slice(IV_SIZE);
|
|
||||||
|
|
||||||
return [JSON.parse(iv), encryptedString];
|
|
||||||
}
|
|
||||||
|
|
||||||
// key
|
|
||||||
export async function importKey(
|
|
||||||
passphrase: string,
|
|
||||||
purpose: "encrypt" | "decrypt"
|
|
||||||
): Promise<CryptoKey> {
|
|
||||||
return await crypto.subtle.importKey(
|
|
||||||
"raw",
|
|
||||||
await hash(encode(passphrase)),
|
|
||||||
{ name: ENCRYPTION_ALG },
|
|
||||||
false,
|
|
||||||
[purpose]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// conversion
|
|
||||||
export function arrayBufferToString(arrayBuffer: ArrayBuffer): string {
|
|
||||||
const uInt8Array = new Uint8Array(arrayBuffer);
|
|
||||||
return decode(uInt8Array);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function uInt8ToArray(uInt8Array: Uint8Array): number[] {
|
|
||||||
return Array.from(uInt8Array);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function arrayToUint8(array: number[]): Uint8Array {
|
|
||||||
return new Uint8Array(array);
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
export const icons = {
|
|
||||||
objectTitle: "label",
|
|
||||||
objectHistory: "history",
|
|
||||||
|
|
||||||
noteContent: "sticky_note_2",
|
|
||||||
categoryName: "category",
|
|
||||||
status: "trending_up",
|
|
||||||
priority: "priority_high",
|
|
||||||
date: "calendar_month",
|
|
||||||
time: "schedule",
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
import "./styles.css";
|
|
||||||
|
|
||||||
import { MessageTab } from "./Tabs/messageTab";
|
|
||||||
import { OverviewTab } from "./Tabs/overviewTab";
|
|
||||||
import { SettingsModal } from "./Views/settingsModal";
|
|
||||||
|
|
||||||
document
|
|
||||||
.querySelector("main")!
|
|
||||||
.append(OverviewTab(), MessageTab(), SettingsModal());
|
|
||||||
document.querySelector("main")!.classList.add("split");
|
|
||||||
-173
@@ -1,173 +0,0 @@
|
|||||||
[unread] {
|
|
||||||
border-left: 10px solid var(--text-warning);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* OBJECTS */
|
|
||||||
:root {
|
|
||||||
--split-thread-height: 8rem;
|
|
||||||
--split-thread-width: 20rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.object-entry-wide {
|
|
||||||
width: 320px;
|
|
||||||
min-width: 320px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#message-tab {
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* standard */
|
|
||||||
.chat-object-view {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* objects */
|
|
||||||
[showingobjects] header {
|
|
||||||
border-bottom: 1px solid var(--border);
|
|
||||||
}
|
|
||||||
|
|
||||||
[showingobjects] footer {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
[showingobjects] .chat-object-view {
|
|
||||||
display: flex;
|
|
||||||
top: var(--header-height);
|
|
||||||
}
|
|
||||||
|
|
||||||
[showingobjects] .thread-view {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* objects split */
|
|
||||||
[showingobjects][chatvisible] .chat-object-view {
|
|
||||||
right: var(--split-thread-width);
|
|
||||||
border-right: 1px solid var(--border);
|
|
||||||
}
|
|
||||||
|
|
||||||
[showingobjects][chatvisible] .thread-view {
|
|
||||||
top: var(--header-height);
|
|
||||||
padding-top: var(--content-padding);
|
|
||||||
}
|
|
||||||
|
|
||||||
[showingobjects][chatvisible] :is(.thread-view, footer) {
|
|
||||||
display: flex;
|
|
||||||
|
|
||||||
right: 0px;
|
|
||||||
left: unset;
|
|
||||||
|
|
||||||
width: var(--split-thread-width);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* hide settings tab */
|
|
||||||
main:has([showingobjects][chatopen]) {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
main:has([showingobjects][chatopen]) #settings-tab {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 640px) {
|
|
||||||
[showingobjects][chatvisible][chatopen] .chat-object-view {
|
|
||||||
width: 100%;
|
|
||||||
bottom: calc(var(--split-thread-height) + var(--control-height));
|
|
||||||
|
|
||||||
border-right: none;
|
|
||||||
border-bottom: 1px solid var(--border);
|
|
||||||
}
|
|
||||||
|
|
||||||
[showingobjects][chatvisible][chatopen] .thread-view {
|
|
||||||
top: unset;
|
|
||||||
bottom: calc(var(--control-height) + var(--safe-area-bottom));
|
|
||||||
|
|
||||||
width: 100%;
|
|
||||||
height: var(--split-thread-height);
|
|
||||||
|
|
||||||
padding: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
[showingobjects][chatvisible][chatopen] footer {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* calendar */
|
|
||||||
:root {
|
|
||||||
--day-view-width: 400px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.calendar-wrapper {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.calendar-wrapper .month-grid {
|
|
||||||
border-bottom: 1px solid var(--border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.calendar-wrapper > * {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.day-miniature[today] h3 {
|
|
||||||
color: var(--primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.day-miniature:has(> div:not(:empty)):not([selected]) {
|
|
||||||
border: var(--gap-between-tiles) solid var(--text-warning);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 750px) {
|
|
||||||
.calendar-wrapper:not([chatvisible] *) {
|
|
||||||
flex-direction: row;
|
|
||||||
}
|
|
||||||
|
|
||||||
.calendar-wrapper:not([chatvisible] *) .month-grid {
|
|
||||||
border-bottom: none;
|
|
||||||
border-right: 1px solid var(--border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.calendar-wrapper:not([chatvisible] *) .day-view {
|
|
||||||
max-width: var(--day-view-width);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 1200px) {
|
|
||||||
.calendar-wrapper {
|
|
||||||
flex-direction: row;
|
|
||||||
}
|
|
||||||
|
|
||||||
.calendar-wrapper .month-grid {
|
|
||||||
border-bottom: none;
|
|
||||||
border-right: 1px solid var(--border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.calendar-wrapper .day-view {
|
|
||||||
max-width: var(--day-view-width);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 600px) or ((min-width: 750px) and (max-width: 1100px)) {
|
|
||||||
.day-miniature {
|
|
||||||
padding: 0px !important;
|
|
||||||
|
|
||||||
align-items: center !important;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.day-miniature h3 {
|
|
||||||
font-size: 1rem;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.day-miniature > div {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,437 +0,0 @@
|
|||||||
const englishTranslations = {
|
|
||||||
// general
|
|
||||||
set: "Set",
|
|
||||||
save: "Save",
|
|
||||||
search: "Search",
|
|
||||||
reset: "Reset",
|
|
||||||
back: "Back",
|
|
||||||
undoChanges: "Undo changes",
|
|
||||||
close: "Close",
|
|
||||||
discard: "Discard",
|
|
||||||
rename: "Rename",
|
|
||||||
|
|
||||||
firstDayOfWeek: "First day of week",
|
|
||||||
weekdays: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"],
|
|
||||||
previousMonth: "previous month",
|
|
||||||
nextMonth: "next month",
|
|
||||||
|
|
||||||
// settings
|
|
||||||
settings: "Settings",
|
|
||||||
showSettings: "Show settings",
|
|
||||||
|
|
||||||
zoomOut: "Zoom Out",
|
|
||||||
zoomIn: "Zoom In",
|
|
||||||
|
|
||||||
repairApp: "Repair App",
|
|
||||||
clearAddresses: "Clear previous connections",
|
|
||||||
clearCategories: "Clear suggestions for object categories",
|
|
||||||
clearStatuses: "Clear suggestions for object statuses",
|
|
||||||
clearObjectFilters: "Clear object filters",
|
|
||||||
|
|
||||||
// overview
|
|
||||||
overview: "Overview",
|
|
||||||
|
|
||||||
connection: "Connection",
|
|
||||||
chats: "Chats",
|
|
||||||
|
|
||||||
yourName: "Your Name",
|
|
||||||
namePlaceholder: "Jane Doe",
|
|
||||||
|
|
||||||
encryptionUnavailableTitle: "Encryption is not available",
|
|
||||||
encryptionUnavailableMessage:
|
|
||||||
"Obtain this app via HTTPS or continue without encryption",
|
|
||||||
|
|
||||||
serverAddress: "Server Address",
|
|
||||||
serverAddressPlaceholder: "wss://192.168.0.69:3000",
|
|
||||||
previousConnections: "Previous connections",
|
|
||||||
|
|
||||||
connectToServer: "Connect",
|
|
||||||
disconnect: "Disconnect",
|
|
||||||
|
|
||||||
mailbox: "Mailbox",
|
|
||||||
requestMailbox: "Enable",
|
|
||||||
deleteMailbox: "Disable",
|
|
||||||
mailboxExplanation:
|
|
||||||
"When you're disconnected, messages will be kept on the server temporarily",
|
|
||||||
|
|
||||||
outbox: "Outbox",
|
|
||||||
outboxText: (count: number) => {
|
|
||||||
switch (count) {
|
|
||||||
case 0:
|
|
||||||
return "All items sent";
|
|
||||||
case 1:
|
|
||||||
return "1 item in outbox";
|
|
||||||
default:
|
|
||||||
return `${count} items in outbox`;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
primaryChannel: "Primary channel",
|
|
||||||
primaryChannelPlaceholder: "my-channel",
|
|
||||||
addChat: "Add",
|
|
||||||
|
|
||||||
// chat
|
|
||||||
showChatOptions: "show chat options",
|
|
||||||
configureChatTitle: "Configure Chat",
|
|
||||||
|
|
||||||
secondaryChannel: "Secondary channel",
|
|
||||||
secondaryChannelPlaceholder: "Add secondary channel",
|
|
||||||
addSecondaryChannel: "Add secondary channel",
|
|
||||||
removeSecondaryChannel: "Remove secondary channel",
|
|
||||||
|
|
||||||
encryptionKey: "Encryption key",
|
|
||||||
encryptionKeyPlaceholder: "n10d2482dg283hg",
|
|
||||||
showKey: "Show key",
|
|
||||||
|
|
||||||
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",
|
|
||||||
|
|
||||||
// objects
|
|
||||||
objectsInTotal: "Objects in total: ",
|
|
||||||
|
|
||||||
showObjects: "show objects",
|
|
||||||
createObject: "create object",
|
|
||||||
untitledObject: "Untitled Object",
|
|
||||||
|
|
||||||
filterObjects: "Filter Objects",
|
|
||||||
searchTitle: "Search",
|
|
||||||
searchPlaceholder: "Note 2000-12-31",
|
|
||||||
searchTitleText: (term: string, resultCount: number) =>
|
|
||||||
term == ""
|
|
||||||
? `Objects in total: ${resultCount}`
|
|
||||||
: `Matches for "${term}": ${resultCount}`,
|
|
||||||
|
|
||||||
viewAll: "All",
|
|
||||||
viewNotes: "Notes",
|
|
||||||
viewKanban: "Kanban",
|
|
||||||
viewStatus: "Status",
|
|
||||||
viewCalendar: "Calendar",
|
|
||||||
|
|
||||||
noObjects: "No objects",
|
|
||||||
|
|
||||||
objectTitle: "Object title",
|
|
||||||
objectTitlePlaceholder: "My object",
|
|
||||||
|
|
||||||
objectVersion: "Object version",
|
|
||||||
note: "Note",
|
|
||||||
noteContentPlaceholder: "Take a note...",
|
|
||||||
category: "Category",
|
|
||||||
categoryPlaceholder: "Define a category",
|
|
||||||
priority: "Priority",
|
|
||||||
priorityPlaceholder: "2",
|
|
||||||
status: "Status",
|
|
||||||
statusPlaceholder: "Pending",
|
|
||||||
date: "Date",
|
|
||||||
time: "Time",
|
|
||||||
|
|
||||||
resendObjects: "Resend all objects",
|
|
||||||
deleteObject: "Delete object",
|
|
||||||
};
|
|
||||||
|
|
||||||
const allTranslations: { [key: string]: typeof englishTranslations } = {
|
|
||||||
en: englishTranslations,
|
|
||||||
es: {
|
|
||||||
// general
|
|
||||||
set: "Guardar",
|
|
||||||
save: "Guardar",
|
|
||||||
search: "Buscar",
|
|
||||||
reset: "Restablecer",
|
|
||||||
back: "Atrás",
|
|
||||||
undoChanges: "Deshacer",
|
|
||||||
close: "Cerrar",
|
|
||||||
discard: "Descartar",
|
|
||||||
rename: "Renombrar",
|
|
||||||
|
|
||||||
firstDayOfWeek: "Primer día de la semana",
|
|
||||||
weekdays: ["Dom", "Lun", "Mar", "Mié", "Jue", "Vie", "Sáb"],
|
|
||||||
previousMonth: "Último mes",
|
|
||||||
nextMonth: "Próximo mes",
|
|
||||||
|
|
||||||
// settings
|
|
||||||
settings: "Configuración",
|
|
||||||
showSettings: "Mostrar configuración",
|
|
||||||
|
|
||||||
zoomOut: "Alejar",
|
|
||||||
zoomIn: "Acercar",
|
|
||||||
|
|
||||||
repairApp: "Reparar Aplicación",
|
|
||||||
clearAddresses: "Borrar conexiones previas",
|
|
||||||
clearCategories: "Borrar sugerencias para categorías de objetos",
|
|
||||||
clearStatuses: "Borrar sugerencias para estados de objetos",
|
|
||||||
clearObjectFilters: "Borrar filtros de objetos",
|
|
||||||
|
|
||||||
// overview
|
|
||||||
overview: "Resumen",
|
|
||||||
|
|
||||||
connection: "Conexión",
|
|
||||||
chats: "Chats",
|
|
||||||
|
|
||||||
yourName: "Tu nombre",
|
|
||||||
namePlaceholder: "Juan Pérez",
|
|
||||||
|
|
||||||
encryptionUnavailableTitle: "Cifrado no disponible",
|
|
||||||
encryptionUnavailableMessage:
|
|
||||||
"Obtén esta aplicación a través de HTTPS o continúa sin cifrado",
|
|
||||||
|
|
||||||
serverAddress: "Dirección del servidor",
|
|
||||||
serverAddressPlaceholder: "wss://192.168.0.69:3000",
|
|
||||||
previousConnections: "Conexiones anteriores",
|
|
||||||
|
|
||||||
connectToServer: "Conectar",
|
|
||||||
disconnect: "Desconectar",
|
|
||||||
|
|
||||||
mailbox: "Buzón",
|
|
||||||
requestMailbox: "Activar",
|
|
||||||
deleteMailbox: "Desactivar",
|
|
||||||
mailboxExplanation:
|
|
||||||
"Si estás sin conexión, los mensajes se guardarán temporalmente en el servidor",
|
|
||||||
|
|
||||||
outbox: "Bandeja de salida",
|
|
||||||
outboxText: (count: number) => {
|
|
||||||
switch (count) {
|
|
||||||
case 0:
|
|
||||||
return "Todos los elementos enviados";
|
|
||||||
case 1:
|
|
||||||
return "1 elemento en bandeja de salida";
|
|
||||||
default:
|
|
||||||
return `${count} elementos en bandeja de salida`;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
primaryChannel: "Canal principal",
|
|
||||||
primaryChannelPlaceholder: "mi-canal",
|
|
||||||
addChat: "Añadir",
|
|
||||||
|
|
||||||
// chat
|
|
||||||
showChatOptions: "Mostrar opciones del chat",
|
|
||||||
configureChatTitle: "Configurar chat",
|
|
||||||
|
|
||||||
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",
|
|
||||||
|
|
||||||
messageInOutbox: "Pendiente",
|
|
||||||
|
|
||||||
noChatSelected: "Selecciona un chat",
|
|
||||||
|
|
||||||
composerPlaceholder: "Escribe un mensaje...",
|
|
||||||
sendMessage: "Enviar",
|
|
||||||
|
|
||||||
// messages
|
|
||||||
resendMessage: "Reenviar mensaje",
|
|
||||||
decryptMessage: "Descifrar mensaje",
|
|
||||||
copyMessage: "Copiar mensaje",
|
|
||||||
deleteMessage: "Eliminar este mensaje",
|
|
||||||
|
|
||||||
// objects
|
|
||||||
objectsInTotal: "Objetos en total: ",
|
|
||||||
|
|
||||||
showObjects: "mostrar objetos",
|
|
||||||
createObject: "añadir nuevo objeto",
|
|
||||||
untitledObject: "Sin título",
|
|
||||||
|
|
||||||
filterObjects: "Filtrar objetos",
|
|
||||||
searchTitle: "Buscar",
|
|
||||||
searchPlaceholder: "Nota 2000-12-31",
|
|
||||||
searchTitleText: (term: string, resultCount: number) =>
|
|
||||||
term == ""
|
|
||||||
? `Objetos en total: ${resultCount}`
|
|
||||||
: `Resultados por "${term}": ${resultCount}`,
|
|
||||||
|
|
||||||
viewAll: "Todos",
|
|
||||||
viewNotes: "Notas",
|
|
||||||
viewKanban: "Kanban",
|
|
||||||
viewStatus: "Estado",
|
|
||||||
viewCalendar: "Calendario",
|
|
||||||
|
|
||||||
noObjects: "Sin objetos",
|
|
||||||
|
|
||||||
objectTitle: "Título",
|
|
||||||
objectTitlePlaceholder: "Mi objeto",
|
|
||||||
|
|
||||||
objectVersion: "Version del objeto",
|
|
||||||
note: "Nota",
|
|
||||||
noteContentPlaceholder: "Toma nota...",
|
|
||||||
category: "Categoría",
|
|
||||||
categoryPlaceholder: "Define una categoría",
|
|
||||||
priority: "Prioridad",
|
|
||||||
priorityPlaceholder: "2",
|
|
||||||
status: "Estado",
|
|
||||||
statusPlaceholder: "Pendiente",
|
|
||||||
date: "Fecha",
|
|
||||||
time: "Hora",
|
|
||||||
|
|
||||||
resendObjects: "Reenviar todos los objetos",
|
|
||||||
deleteObject: "Eliminar objeto",
|
|
||||||
},
|
|
||||||
de: {
|
|
||||||
// general
|
|
||||||
set: "OK",
|
|
||||||
save: "Sichern",
|
|
||||||
search: "Suchen",
|
|
||||||
reset: "Zurücksetzen",
|
|
||||||
back: "Zurück",
|
|
||||||
undoChanges: "Änderungen verwerfen",
|
|
||||||
close: "Schließen",
|
|
||||||
discard: "Verwerfen",
|
|
||||||
rename: "Umbenennen",
|
|
||||||
|
|
||||||
firstDayOfWeek: "Erster Wochentag",
|
|
||||||
weekdays: ["So", "Mo", "Di", "Mi", "Do", "Fr", "Sa"],
|
|
||||||
previousMonth: "Vorheriger Monat",
|
|
||||||
nextMonth: "Nächster Monat",
|
|
||||||
|
|
||||||
// settings
|
|
||||||
settings: "Einstellungen",
|
|
||||||
showSettings: "Einstellungen anzeigen",
|
|
||||||
|
|
||||||
zoomOut: "Verkleinern",
|
|
||||||
zoomIn: "Vergrößern",
|
|
||||||
|
|
||||||
repairApp: "App reparieren",
|
|
||||||
clearAddresses: "Vorherige Verbindungen löschen",
|
|
||||||
clearCategories: "Vorschläge für Objektkategorien löschen",
|
|
||||||
clearStatuses: "Vorschläge für Objektstaten löschen",
|
|
||||||
clearObjectFilters: "Objektfilter löschen",
|
|
||||||
|
|
||||||
// overview
|
|
||||||
overview: "Übersicht",
|
|
||||||
|
|
||||||
connection: "Verbindung",
|
|
||||||
chats: "Chats",
|
|
||||||
|
|
||||||
yourName: "Dein Name",
|
|
||||||
namePlaceholder: "Max Mustermann",
|
|
||||||
|
|
||||||
encryptionUnavailableTitle: "Verschlüsselung nicht möglich",
|
|
||||||
encryptionUnavailableMessage:
|
|
||||||
"Lade diese app über HTTPS, um Nachrichten zu verschlüsseln",
|
|
||||||
|
|
||||||
serverAddress: "Serveradresse",
|
|
||||||
serverAddressPlaceholder: "wss://192.168.0.69:3000",
|
|
||||||
previousConnections: "Vorherige Verbindungen",
|
|
||||||
|
|
||||||
connectToServer: "Verbinden",
|
|
||||||
disconnect: "Trennen",
|
|
||||||
|
|
||||||
mailbox: "Briefkasten",
|
|
||||||
requestMailbox: "Aktivieren",
|
|
||||||
deleteMailbox: "Deaktivieren",
|
|
||||||
mailboxExplanation:
|
|
||||||
"Wenn du offline bist, werden Nachrichten auf dem Server gelagert",
|
|
||||||
|
|
||||||
outbox: "Ausgang",
|
|
||||||
outboxText: (count: number) => {
|
|
||||||
switch (count) {
|
|
||||||
case 0:
|
|
||||||
return "Alle Nachrichten gesendet";
|
|
||||||
case 1:
|
|
||||||
return "Eine Nachricht ausstehend";
|
|
||||||
default:
|
|
||||||
return `${count} Nachrichten ausstehend`;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
primaryChannel: "Hauptkanal",
|
|
||||||
primaryChannelPlaceholder: "mein-kanal",
|
|
||||||
addChat: "Hinzufügen",
|
|
||||||
|
|
||||||
// chat
|
|
||||||
showChatOptions: "Chatoptionen einblenden",
|
|
||||||
configureChatTitle: "Chat konfigurieren",
|
|
||||||
|
|
||||||
secondaryChannel: "Zweitkanal",
|
|
||||||
secondaryChannelPlaceholder: "Zweitkanal hinzufügen",
|
|
||||||
addSecondaryChannel: "Zweitkanal hinzufügen",
|
|
||||||
removeSecondaryChannel: "Zweitkanal entfernen",
|
|
||||||
|
|
||||||
encryptionKey: "Schlüssel",
|
|
||||||
encryptionKeyPlaceholder: "n10d2482dg283hg",
|
|
||||||
showKey: "Schlüssel anzeigen",
|
|
||||||
|
|
||||||
clearObjects: "Alle Objekte löschen",
|
|
||||||
clearChatMessages: "Nachrichtenverlauf leeren",
|
|
||||||
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
|
|
||||||
objectsInTotal: "Objekte insgesamt: ",
|
|
||||||
|
|
||||||
showObjects: "Objekte anzeigen",
|
|
||||||
createObject: "Neues Objekt erstellen",
|
|
||||||
untitledObject: "Unbenannt",
|
|
||||||
|
|
||||||
filterObjects: "Objekte filtern",
|
|
||||||
searchTitle: "Suchen",
|
|
||||||
searchPlaceholder: "Notiz 2000-12-31",
|
|
||||||
searchTitleText: (term: string, resultCount: number) =>
|
|
||||||
term == ""
|
|
||||||
? `Objekte insgesamt: ${resultCount}`
|
|
||||||
: `Ergebnisse für "${term}": ${resultCount}`,
|
|
||||||
|
|
||||||
viewAll: "Alle",
|
|
||||||
viewNotes: "Notizen",
|
|
||||||
viewKanban: "Kanban",
|
|
||||||
viewStatus: "Status",
|
|
||||||
viewCalendar: "Kalender",
|
|
||||||
|
|
||||||
noObjects: "Keine Objekte",
|
|
||||||
|
|
||||||
objectTitle: "Titel",
|
|
||||||
objectTitlePlaceholder: "Mein Objekt",
|
|
||||||
|
|
||||||
objectVersion: "Version des Objekts",
|
|
||||||
note: "Notiz",
|
|
||||||
noteContentPlaceholder: "Notiz eingeben...",
|
|
||||||
category: "Kategorie",
|
|
||||||
categoryPlaceholder: "Kategorie eingeben...",
|
|
||||||
priority: "Priorität",
|
|
||||||
priorityPlaceholder: "2",
|
|
||||||
status: "Status",
|
|
||||||
statusPlaceholder: "Ausstehend",
|
|
||||||
date: "Datum",
|
|
||||||
time: "Uhrzeit",
|
|
||||||
|
|
||||||
resendObjects: "Objekte erneut senden",
|
|
||||||
deleteObject: "Objekt löschen",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const language = navigator.language.substring(0, 2);
|
|
||||||
export const translation: typeof englishTranslations =
|
|
||||||
allTranslations[language] ?? allTranslations.en;
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
import * as React from "bloatless-react";
|
|
||||||
|
|
||||||
import { MessageObjectContent } from "./Model/chatModel";
|
|
||||||
|
|
||||||
export const storageKeys = {
|
|
||||||
viewType(id: string): string {
|
|
||||||
return id + "view-type";
|
|
||||||
},
|
|
||||||
|
|
||||||
hasUnread(id: string): string {
|
|
||||||
return id + "has-unread-messages";
|
|
||||||
},
|
|
||||||
|
|
||||||
primaryChannel(id: string): string {
|
|
||||||
return id + "primary-channel";
|
|
||||||
},
|
|
||||||
|
|
||||||
secondaryChannels(id: string): string {
|
|
||||||
return id + "secondary-channels";
|
|
||||||
},
|
|
||||||
|
|
||||||
encyptionKey(id: string): string {
|
|
||||||
return id + "encryption-key";
|
|
||||||
},
|
|
||||||
|
|
||||||
messages(id: string): string {
|
|
||||||
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";
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const stringToOptionTag: React.StateItemConverter<string> = (value) => {
|
|
||||||
return <option>{value}</option>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function getRawObjectIndex(latest: MessageObjectContent): string {
|
|
||||||
// date
|
|
||||||
const dateString = latest.date || "0000-00-00";
|
|
||||||
const [year, month, date] = dateString.split("-")
|
|
||||||
|
|
||||||
// time
|
|
||||||
const timeString = latest.time || "00:00";
|
|
||||||
const [hour, minute] = timeString.split(":");
|
|
||||||
|
|
||||||
const hourInMinutes = parseInt(hour) * 60;
|
|
||||||
const minutesTotal = parseInt(minute) + hourInMinutes;
|
|
||||||
const paddedMinutes = minutesTotal.toString().padStart(4, "0")
|
|
||||||
|
|
||||||
// priority
|
|
||||||
const priority = latest.priority ?? 0;
|
|
||||||
const priorityInverse = 100 - priority;
|
|
||||||
const paddedPriority = priorityInverse.toString().padStart(3, "0");
|
|
||||||
|
|
||||||
// final
|
|
||||||
return `${year}${month}${date}${paddedMinutes}${paddedPriority}`;
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user