diff --git a/src/Model/chatModel.ts b/src/Model/chatModel.ts deleted file mode 100644 index 2b078746..00000000 --- a/src/Model/chatModel.ts +++ /dev/null @@ -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; -} - -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; - - isSubscribed = new React.State(false); - primaryChannel = new React.State(""); - hasUnreadMessages: React.State; - secondaryChannels: React.ListState; - - encryptionKey: React.State; - messages: React.ListState; - objects: React.MapState; - outbox: React.ListState; - objectOutbox: React.MapState; - isOutBoxEmpty: React.State; - - composingMessage: React.State; - primaryChannelInput: React.State; - newSecondaryChannelName: React.State; - - cannotSendMessage: React.State; - cannotResendMessage: React.State; - cannotAddSecondaryChannel: React.State; - cannotSetChannel: React.State; - cannotUndoChannel: React.State; - cannotClearMessages: React.State; - cannotClearObjects: React.State; - - // init - constructor(id: string = React.UUID()) { - this.id = id; - - this.viewType = React.restoreState( - 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 => { - 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 => { - // 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 => { - if (this.cannotSendMessage.value == true) return; - - await this.sendMessageFromText(this.composingMessage.value); - this.composingMessage.value = ""; - }; - - sendMessageFromText = async (text: string): Promise => { - 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 => { - chatMessage.body = await decryptString( - chatMessage.body, - this.encryptionKey.value - ); - this.messages.callSubscriptions(); - }; - - sendMessage = async (chatMessage: ChatMessage): Promise => { - 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(); -} diff --git a/src/Model/model.ts b/src/Model/model.ts deleted file mode 100644 index aab6c0c6..00000000 --- a/src/Model/model.ts +++ /dev/null @@ -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("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("object-categories"); -export const usedObjectStatuses = - React.restoreListState("object-statuses"); -export const previousObjectSearches = - React.restoreListState("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) { - const lowerCased = [...listState.value].map((x) => x.toLowerCase()); - listState.clear(); - listState.add(...lowerCased); -} - -function removeSetDuplicates(listState: React.ListState) { - 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(); -export const chatIds = React.restoreListState("chat-ids"); - -export const selectedChat = new React.State(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(); -} diff --git a/src/Tabs/messageTab.tsx b/src/Tabs/messageTab.tsx deleted file mode 100644 index 648c202f..00000000 --- a/src/Tabs/messageTab.tsx +++ /dev/null @@ -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 ( -
- {translation.noChatSelected} -
- ); - - const chat = selectedChat.value; - const isShowingOptions = new React.State(false); - - function showOptions() { - isShowingOptions.value = true; - } - - return [ -
- - - - - - - - - - -
, - - ObjectPane(chat), - ThreadView(chat), - -
{MessageComposer(chat)}
, - - ChatOptionModal(chat, isShowingOptions), - ]; - }); - - return ( -
- ); -} diff --git a/src/Tabs/overviewTab.tsx b/src/Tabs/overviewTab.tsx deleted file mode 100644 index 983103b4..00000000 --- a/src/Tabs/overviewTab.tsx +++ /dev/null @@ -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 ( -
-
- {translation.overview} - -
- -
-
- warning -
- {translation.encryptionUnavailableTitle} - - {translation.encryptionUnavailableMessage} - -
-
- - {PersonalSection()} - {ConnectionSection()} - {ChatListSection()} -
-
- ); -} diff --git a/src/Views/Chat/chatOptionModal.tsx b/src/Views/Chat/chatOptionModal.tsx deleted file mode 100644 index d845f99d..00000000 --- a/src/Views/Chat/chatOptionModal.tsx +++ /dev/null @@ -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) { - 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 = ( - secondaryChannel - ) => { - function remove() { - chat.removeSecondaryChannel(secondaryChannel); - } - - return ( -
-
- {secondaryChannel} - -
-
- ); - }; - - return ( - - ); -} diff --git a/src/Views/Chat/messageComposer.tsx b/src/Views/Chat/messageComposer.tsx deleted file mode 100644 index 61b839ca..00000000 --- a/src/Views/Chat/messageComposer.tsx +++ /dev/null @@ -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 ( -
- {" "} - - -
- ); -} diff --git a/src/Views/Chat/threadView.tsx b/src/Views/Chat/threadView.tsx deleted file mode 100644 index 96683859..00000000 --- a/src/Views/Chat/threadView.tsx +++ /dev/null @@ -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 = (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 ( -
-
-
- - {message.sender} - - - - - - - -
-
- - - {new Date(message.isoDate).toLocaleString()} -

- {message.channel} -
-
-
-
- ); - }; - - const outboxMessageConverter: React.StateItemConverter = ( - message - ) => { - function remove() { - chat.deleteOutboxMessage(message); - } - - return ( -
-
-
- {translation.messageInOutbox} - - - -
-
- {message.body} - - {new Date(message.isoDate).toLocaleString()} -

- {message.channel} -
-
-
-
- ); - }; - - const messageList = ( -
- ); - const outboxList = ( -
- ); - const listWrapper = ( -
- {messageList} - {outboxList} -
- ); - - 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; -} diff --git a/src/Views/Objects/allObjectsView.tsx b/src/Views/Objects/allObjectsView.tsx deleted file mode 100644 index bba231e0..00000000 --- a/src/Views/Objects/allObjectsView.tsx +++ /dev/null @@ -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, - selectedObject: React.State, - isShowingObjectModal: React.State -) { - const objectCount = React.createProxyState( - [chat.objects], - () => chat.objects.value.size - ); - - return ( -
- - {translation.objectsInTotal}{" "} - - - {ObjectGridView( - chat, - messageObjects, - selectedObject, - isShowingObjectModal - )} -
- ); -} diff --git a/src/Views/Objects/calendarView.tsx b/src/Views/Objects/calendarView.tsx deleted file mode 100644 index f40c53e1..00000000 --- a/src/Views/Objects/calendarView.tsx +++ /dev/null @@ -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, - selectedObject: React.State, - isShowingObjectModal: React.State -) { - 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([]); - 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( - - {translation.weekdays[currentLabelIndex]} - - ); - 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(
); - } - - // add day views - while (currentDate.getUTCMonth() == newSelectedDate.getUTCMonth()) { - monthGridCells.value.push( - MonthGridCell(chat, messageObjects, currentDate) - ); - currentDate.setDate(currentDate.getDate() + 1); - } - - monthGridCells.callSubscriptions(); - } - - return ( -
-
-
- - - - -
-
-
-
- {DayView(chat, messageObjects, selectedObject, isShowingObjectModal)} -
- ); -} diff --git a/src/Views/Objects/dayView.tsx b/src/Views/Objects/dayView.tsx deleted file mode 100644 index 65a0d16f..00000000 --- a/src/Views/Objects/dayView.tsx +++ /dev/null @@ -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 - | React.MapState, - selectedObject: React.State, - isShowingObjectModal: React.State -) { - const objectsForDayView = new React.ListState(); - - 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 = ( - messageObject - ) => { - return ObjectEntryView( - chat, - messageObject, - selectedObject, - isShowingObjectModal - ); - }; - - return ( -
- ); -} diff --git a/src/Views/Objects/kanbanView.tsx b/src/Views/Objects/kanbanView.tsx deleted file mode 100644 index 95d76ed5..00000000 --- a/src/Views/Objects/kanbanView.tsx +++ /dev/null @@ -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; -} - -interface KanbanBoardItem { - priority: number; - messageObject: MessageObjectWithIndex; -} - -export function KanbanView( - chat: Chat, - messageObjects: React.MapState, - selectedObject: React.State, - isShowingObjectModal: React.State -) { - const boards = new React.MapState(); - 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(); - 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 = (board) => { - const view = KanbanBoardView( - chat, - board, - selectedObject, - isShowingObjectModal - ); - sortedBoardNames.subscribe((sortedBoardNames) => { - view.style.order = sortedBoardNames.indexOf(board.category); - }); - return view; - }; - - return ( -
- ); -} - -function KanbanBoardView( - chat: Chat, - kanbanBoard: KanbanBoard, - selectedObject: React.State, - isShowingObjectModal: React.State -) { - 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 - ) => { - return ObjectEntryView( - chat, - kanbanBoardItem.messageObject, - selectedObject, - isShowingObjectModal - ); - }; - - return ( -
- {RenameView(editingCategory, kanbanBoard.category, renameBoard)} -
-
-
- ); -} diff --git a/src/Views/Objects/monthGridCell.tsx b/src/Views/Objects/monthGridCell.tsx deleted file mode 100644 index 2f9bdf66..00000000 --- a/src/Views/Objects/monthGridCell.tsx +++ /dev/null @@ -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 - | React.MapState, - dateObject: Date -) { - const objectsForDayView = new React.ListState(); - - 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 = ( - messageObject - ) => { - const view = ( - - {chat.getObjectTitle(messageObject)} - - ); - 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 ( - - ); -} diff --git a/src/Views/Objects/noteObjectsView.tsx b/src/Views/Objects/noteObjectsView.tsx deleted file mode 100644 index a6199eb6..00000000 --- a/src/Views/Objects/noteObjectsView.tsx +++ /dev/null @@ -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, - selectedObject: React.State, - isShowingObjectModal: React.State -) { - const notes = new React.ListState(); - messageObjects.handleAddition((messageObject) => { - const latest = chat.getMostRecentContent(messageObject); - if (!latest.noteContent) return; - - notes.add(messageObject); - messageObjects.handleRemoval(messageObject, () => - notes.remove(messageObject) - ); - }); - - return
- {ObjectGridView(chat, notes, selectedObject, isShowingObjectModal)} -
; -} diff --git a/src/Views/Objects/objectDetailModal.tsx b/src/Views/Objects/objectDetailModal.tsx deleted file mode 100644 index 3f402f96..00000000 --- a/src/Views/Objects/objectDetailModal.tsx +++ /dev/null @@ -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 -) { - // 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 = ( - - ); - isPresented.subscribe(() => setTimeout(() => input.focus(), 100)); - - return ( - - ); -} diff --git a/src/Views/Objects/objectEntryView.tsx b/src/Views/Objects/objectEntryView.tsx deleted file mode 100644 index 47b43cba..00000000 --- a/src/Views/Objects/objectEntryView.tsx +++ /dev/null @@ -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, - isShowingObjectModal: React.State -) { - 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 ( - - {icon} - {value} - - ); - }); - - const view = ( - - ); - messageObject.index.subscribe(newIndex => { - view.style.order = newIndex; - }) - return view; -} diff --git a/src/Views/Objects/objectGridView.tsx b/src/Views/Objects/objectGridView.tsx deleted file mode 100644 index 85dd7b75..00000000 --- a/src/Views/Objects/objectGridView.tsx +++ /dev/null @@ -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 - | React.MapState, - selectedObject: React.State, - isShowingObjectModal: React.State -) { - const objectConverter: React.StateItemConverter = ( - messageObject - ) => { - return ObjectEntryView( - chat, - messageObject, - selectedObject, - isShowingObjectModal - ); - }; - - return ( -
- ); -} diff --git a/src/Views/Objects/objectPane.tsx b/src/Views/Objects/objectPane.tsx deleted file mode 100644 index ebd97357..00000000 --- a/src/Views/Objects/objectPane.tsx +++ /dev/null @@ -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(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(); - 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
; - - 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 ( -
-
- - -
- {...Object.keys(viewTypes).map((key: any) => - ViewTypeToggle(key, chat.viewType) - )} -
- - -
- -
- - -
-
- ); -} - -function ViewTypeToggle( - key: keyof typeof viewTypes, - selection: React.State -) { - const [label, icon] = viewTypes[key]; - - function select() { - selection.value = key; - } - - const isSelected = React.createProxyState( - [selection], - () => selection.value == key - ); - - return ( - - ); -} diff --git a/src/Views/Objects/placeholderView.tsx b/src/Views/Objects/placeholderView.tsx deleted file mode 100644 index ffb82117..00000000 --- a/src/Views/Objects/placeholderView.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import * as React from "bloatless-react"; - -import { translation } from "../../translations"; - -export function PlaceholderView() { - return
- {translation.noObjects} -
; -} diff --git a/src/Views/Objects/statusView.tsx b/src/Views/Objects/statusView.tsx deleted file mode 100644 index ca68e2fa..00000000 --- a/src/Views/Objects/statusView.tsx +++ /dev/null @@ -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; -} - -interface StatusColumn { - status: string; - items: React.ListState; -} - -interface StatusCellItem { - priority: number; - status: string; - messageObject: MessageObjectWithIndex; -} - -// data only -interface StatusData { - title: string; - items: MessageObject[]; -} - -export function StatusView( - chat: Chat, - messageObjects: React.MapState, - selectedObject: React.State, - isShowingObjectModal: React.State -) { - const rows = new React.MapState(); - const statuses = new React.MapState(); - 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 - ) => { - const view = StatusHeaderView(chat, statusData); - sortedStatuses.subscribe( - () => (view.style.order = sortedStatuses.value.indexOf(statusData.title)) - ); - return view; - }; - - const categoryRowToView: React.StateItemConverter = (row) => { - const view = KanbanRowView( - chat, - row, - sortedStatuses, - selectedObject, - isShowingObjectModal - ); - sortedCategoryNames.subscribe( - () => (view.style.order = sortedCategoryNames.value.indexOf(row.category)) - ); - return view; - }; - - return ( -
-
-
-
-
-
-
- ); -} - -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 ( -
- {RenameView(editingStatus, status.title, rename)} -
-
- ); -} - -function KanbanRowView( - chat: Chat, - kanbanRow: CategoryRow, - sortedStatuses: React.State, - selectedObject: React.State, - isShowingObjectModal: React.State -) { - 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 - ) => { - 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 ( -
-
- {RenameView(editingCategory, kanbanRow.category, renameCategory)} -
- -
-
- ); -} - -function StatusColumnView( - chat: Chat, - items: React.ListState, - category: string, - status: string, - selectedObject: React.State, - isShowingObjectModal: React.State -) { - 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 = ( - cellItem - ) => - ObjectEntryView( - chat, - cellItem.messageObject, - selectedObject, - isShowingObjectModal - ); - - return ( -
- ); -} diff --git a/src/Views/Overview/chatListSection.tsx b/src/Views/Overview/chatListSection.tsx deleted file mode 100644 index 3e57b4bc..00000000 --- a/src/Views/Overview/chatListSection.tsx +++ /dev/null @@ -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) => { - function select() { - selectChat(chat); - } - - const isSelected = React.createProxyState( - [selectedChat], - () => selectedChat.value == chat - ); - - return ( - - ); -}; - -export function ChatListSection() { - return ( -
-

{translation.chats}

- - -
- -
- -
- -
-
- ); -} diff --git a/src/Views/Overview/connectionSection.tsx b/src/Views/Overview/connectionSection.tsx deleted file mode 100644 index 22009e8d..00000000 --- a/src/Views/Overview/connectionSection.tsx +++ /dev/null @@ -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 = ( - value - ) => { - const isSelected = value == serverAddressInput.value; - return ; - }; - - return ( -
-

{translation.connection}

- - - -
- - - -
- -
- -
- inbox -
- {translation.mailbox} - {translation.mailboxExplanation} -
-
-
- - -
- -
- -
- outbox -
- {translation.outbox} - -
-
-
- ); -} diff --git a/src/Views/Overview/personalSection.tsx b/src/Views/Overview/personalSection.tsx deleted file mode 100644 index cab91625..00000000 --- a/src/Views/Overview/personalSection.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import * as React from "bloatless-react"; - -import { senderName } from "../../Model/model"; -import { translation } from "../../translations"; - -export function PersonalSection() { - return ( -
- -
- ); -} diff --git a/src/Views/renameView.tsx b/src/Views/renameView.tsx deleted file mode 100644 index 33b4b5d9..00000000 --- a/src/Views/renameView.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import * as React from "bloatless-react"; - -import { translation } from "../translations"; - -export function RenameView( - editingName: React.State, - initialName: string, - rename: () => void -) { - const cannotRename = React.createProxyState( - [editingName], - () => editingName.value == initialName || editingName.value == "" - ); - - return ( -
- - -
- ); -} diff --git a/src/Views/settingsModal.tsx b/src/Views/settingsModal.tsx deleted file mode 100644 index 3e57e10d..00000000 --- a/src/Views/settingsModal.tsx +++ /dev/null @@ -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 = (value) => { - const isSelected = value == firstDayOfWeek.value; - return ; - }; - - return ( - - ); -} diff --git a/src/cryptUtility.ts b/src/cryptUtility.ts deleted file mode 100644 index f839e0a3..00000000 --- a/src/cryptUtility.ts +++ /dev/null @@ -1,115 +0,0 @@ -const IV_SIZE = 12; -const ENCRYPTION_ALG = "AES-GCM"; - -// minimal -export async function encryptString( - plaintext: string, - passphrase: string -): Promise { - 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 { - 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 { - 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 { - const arrayBuffer = await crypto.subtle.decrypt( - { name: ENCRYPTION_ALG, iv }, - key, - cyphertext - ); - return arrayBufferToString(arrayBuffer); -} - -export async function hash(encoded: Uint8Array): Promise { - 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 { - 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); -} diff --git a/src/icons.ts b/src/icons.ts deleted file mode 100644 index c19233a3..00000000 --- a/src/icons.ts +++ /dev/null @@ -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", -} \ No newline at end of file diff --git a/src/index.tsx b/src/index.tsx deleted file mode 100644 index eedbf3cf..00000000 --- a/src/index.tsx +++ /dev/null @@ -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"); \ No newline at end of file diff --git a/src/styles.css b/src/styles.css deleted file mode 100644 index 8eb25fd8..00000000 --- a/src/styles.css +++ /dev/null @@ -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; - } -} diff --git a/src/translations.ts b/src/translations.ts deleted file mode 100644 index 42f3245d..00000000 --- a/src/translations.ts +++ /dev/null @@ -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; diff --git a/src/utility.tsx b/src/utility.tsx deleted file mode 100644 index ca32dcf7..00000000 --- a/src/utility.tsx +++ /dev/null @@ -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 = (value) => { - return ; -}; - -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}`; -}