add basic thread view
This commit is contained in:
Vendored
+7
@@ -369,6 +369,13 @@ body[showing-chat] #chat-page-wrapper {
|
|||||||
max-width: unset;
|
max-width: unset;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
#message-page #message-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: var(--content-padding);
|
||||||
|
gap: var(--gap-between-tiles);
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
||||||
|
|
||||||
/* src/View/Components/chatEntry.css */
|
/* src/View/Components/chatEntry.css */
|
||||||
.chat-entry {
|
.chat-entry {
|
||||||
|
|||||||
Vendored
+147
-49
@@ -201,13 +201,13 @@
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "append":
|
case "append":
|
||||||
case "appendandscroll":
|
|
||||||
case "prepend": {
|
case "prepend": {
|
||||||
element.style.scrollBehavior = "smooth";
|
|
||||||
try {
|
try {
|
||||||
const [listState, toElement] = value;
|
const [listState, toElement] = value;
|
||||||
listState.handleAddition((newItem) => {
|
listState.handleAddition((newItem) => {
|
||||||
|
console.log(newItem, toElement);
|
||||||
const child = toElement(newItem);
|
const child = toElement(newItem);
|
||||||
|
console.log(child);
|
||||||
listState.handleRemoval(
|
listState.handleRemoval(
|
||||||
newItem,
|
newItem,
|
||||||
() => child.remove()
|
() => child.remove()
|
||||||
@@ -218,8 +218,10 @@
|
|||||||
element.prepend(child);
|
element.prepend(child);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch {
|
} catch (error) {
|
||||||
throw `error: cannot process subscribe:children directive because StateItemConverter is not defined. Usage: "subscribe:children={[list, converter]}"; you can find a more detailed example in the documentation`;
|
throw `error: cannot process subscribe:children directive.
|
||||||
|
Error: ${error}
|
||||||
|
Usage: "subscribe:children={[list, converter]}"; you can find a more detailed example in the documentation.`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -392,8 +394,8 @@
|
|||||||
id;
|
id;
|
||||||
info;
|
info;
|
||||||
color;
|
color;
|
||||||
messages = /* @__PURE__ */ new Set();
|
chatMessageHandler = () => {
|
||||||
objects = /* @__PURE__ */ new Map();
|
};
|
||||||
// sorting
|
// sorting
|
||||||
get index() {
|
get index() {
|
||||||
return this.chatListModel.getIndexOfPrimaryChannel(
|
return this.chatListModel.getIndexOfPrimaryChannel(
|
||||||
@@ -424,6 +426,7 @@
|
|||||||
this.info.primaryChannel = primaryChannel;
|
this.info.primaryChannel = primaryChannel;
|
||||||
this.storeInfo();
|
this.storeInfo();
|
||||||
this.chatListModel.updateIndices();
|
this.chatListModel.updateIndices();
|
||||||
|
this.subscribe();
|
||||||
};
|
};
|
||||||
setSecondaryChannels = (secondaryChannels) => {
|
setSecondaryChannels = (secondaryChannels) => {
|
||||||
this.info.secondaryChannels = secondaryChannels;
|
this.info.secondaryChannels = secondaryChannels;
|
||||||
@@ -444,21 +447,17 @@
|
|||||||
storeColor = () => {
|
storeColor = () => {
|
||||||
this.storageModel.store(this.colorPath, this.color);
|
this.storageModel.store(this.colorPath, this.color);
|
||||||
};
|
};
|
||||||
addMessage = (message) => {
|
addMessage = (chatMessage) => {
|
||||||
if (!this.messages.has(message)) {
|
const messagePath = this.getMessagePath(chatMessage.id);
|
||||||
this.messages.add(message);
|
this.storageModel.storeStringifiable(messagePath, chatMessage);
|
||||||
}
|
this.chatMessageHandler(chatMessage);
|
||||||
const messagePath = this.getMessagePath(message.id);
|
|
||||||
this.storageModel.storeStringifiable(messagePath, message);
|
|
||||||
};
|
};
|
||||||
addObject = (object) => {
|
addObject = (chatObject) => {
|
||||||
this.objects.set(object.id, object);
|
const objectPath = this.getObjectPath(chatObject.id);
|
||||||
const objectPath = this.getObjectPath(object.id);
|
this.storageModel.storeStringifiable(objectPath, chatObject);
|
||||||
this.storageModel.storeStringifiable(objectPath, object);
|
|
||||||
};
|
};
|
||||||
// delete
|
// delete
|
||||||
delete = () => {
|
delete = () => {
|
||||||
this.unloadData();
|
|
||||||
this.chatListModel.untrackChat(this);
|
this.chatListModel.untrackChat(this);
|
||||||
const dirPath = [...storageKeys.chats, this.id];
|
const dirPath = [...storageKeys.chats, this.id];
|
||||||
this.storageModel.removeRecursive(dirPath);
|
this.storageModel.removeRecursive(dirPath);
|
||||||
@@ -481,27 +480,31 @@
|
|||||||
this.color = color;
|
this.color = color;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
restoreMessages = () => {
|
get messages() {
|
||||||
const messageIds = this.storageModel.list(this.messageDirPath);
|
const messageIds = this.storageModel.list(this.messageDirPath);
|
||||||
if (!Array.isArray(messageIds)) return;
|
if (!Array.isArray(messageIds)) return [];
|
||||||
|
let messages = [];
|
||||||
for (const messageId of messageIds) {
|
for (const messageId of messageIds) {
|
||||||
const messagePath = this.getMessagePath(messageId);
|
const messagePath = this.getMessagePath(messageId);
|
||||||
const message = this.storageModel.restoreStringifiable(messagePath);
|
const message = this.storageModel.restoreStringifiable(messagePath);
|
||||||
if (checkIsValidObject(message) == false) return;
|
if (checkIsValidObject(message) == false) continue;
|
||||||
this.messages.add(message);
|
messages.push(message);
|
||||||
}
|
}
|
||||||
};
|
return messages;
|
||||||
restoreObjects = () => {
|
}
|
||||||
|
get objects() {
|
||||||
const objectIds = this.storageModel.list(this.objectDirPath);
|
const objectIds = this.storageModel.list(this.objectDirPath);
|
||||||
if (!Array.isArray(objectIds)) return;
|
if (!Array.isArray(objectIds)) return [];
|
||||||
|
let objects = [];
|
||||||
for (const objectId of objectIds) {
|
for (const objectId of objectIds) {
|
||||||
const objectPathComponents = this.getObjectPath(objectId);
|
const objectPathComponents = this.getObjectPath(objectId);
|
||||||
const object = this.storageModel.restoreStringifiable(objectPathComponents);
|
const object = this.storageModel.restoreStringifiable(objectPathComponents);
|
||||||
if (checkIsValidObject(object) == false) return;
|
if (checkIsValidObject(object) == false) continue;
|
||||||
if (object.id != objectId) return;
|
if (object.id != objectId) continue;
|
||||||
this.objects.set(objectId, object);
|
objects.push(object);
|
||||||
}
|
}
|
||||||
};
|
return objects;
|
||||||
|
}
|
||||||
// messaging
|
// messaging
|
||||||
sendMessage = (body) => {
|
sendMessage = (body) => {
|
||||||
const senderName = this.settingsModel.username;
|
const senderName = this.settingsModel.username;
|
||||||
@@ -519,14 +522,16 @@
|
|||||||
this.connectionModel.sendMessageOrStore(chatMessage);
|
this.connectionModel.sendMessageOrStore(chatMessage);
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
// memory
|
handleMessage = (channel, body) => {
|
||||||
loadData = () => {
|
const chatMessage = _ChatModel.parseMessage(body);
|
||||||
this.restoreMessages();
|
if (chatMessage == null) return;
|
||||||
this.restoreObjects();
|
this.addMessage(chatMessage);
|
||||||
};
|
};
|
||||||
unloadData = () => {
|
setMessageHandler = (handler) => {
|
||||||
this.messages.clear();
|
this.chatMessageHandler = handler;
|
||||||
this.objects.clear();
|
};
|
||||||
|
subscribe = () => {
|
||||||
|
this.connectionModel.addChannel(this.info.primaryChannel);
|
||||||
};
|
};
|
||||||
// init
|
// init
|
||||||
constructor(storageModel2, connectionModel2, settingsModel2, chatListModel2, chatId) {
|
constructor(storageModel2, connectionModel2, settingsModel2, chatListModel2, chatId) {
|
||||||
@@ -537,6 +542,7 @@
|
|||||||
this.chatListModel = chatListModel2;
|
this.chatListModel = chatListModel2;
|
||||||
this.restoreInfo();
|
this.restoreInfo();
|
||||||
this.restoreColor();
|
this.restoreColor();
|
||||||
|
this.subscribe();
|
||||||
}
|
}
|
||||||
// utility
|
// utility
|
||||||
static generateChatInfo = (primaryChannel) => {
|
static generateChatInfo = (primaryChannel) => {
|
||||||
@@ -557,6 +563,15 @@
|
|||||||
dateSent: createTimestamp()
|
dateSent: createTimestamp()
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
static parseMessage = (string) => {
|
||||||
|
try {
|
||||||
|
const parsed = parse(string);
|
||||||
|
if (checkIsValidObject(parsed) == false) return null;
|
||||||
|
return parsed;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// src/Model/chatListModel.ts
|
// src/Model/chatListModel.ts
|
||||||
@@ -567,6 +582,21 @@
|
|||||||
// data
|
// data
|
||||||
chatModels = /* @__PURE__ */ new Set();
|
chatModels = /* @__PURE__ */ new Set();
|
||||||
sortedPrimaryChannels = [];
|
sortedPrimaryChannels = [];
|
||||||
|
// handlers
|
||||||
|
messageHandler = (data) => {
|
||||||
|
for (const chatModel of this.chatModels) {
|
||||||
|
const channel = data.messageChannel;
|
||||||
|
const body = data.messageBody;
|
||||||
|
if (channel == void 0) return;
|
||||||
|
if (body == void 0) return;
|
||||||
|
const allChannels = channel.split("/");
|
||||||
|
for (const channel2 of allChannels) {
|
||||||
|
if (channel2 != chatModel.info.primaryChannel) continue;
|
||||||
|
chatModel.handleMessage(channel2, body);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
// store & add
|
// store & add
|
||||||
addChatModel = (chatModel) => {
|
addChatModel = (chatModel) => {
|
||||||
this.chatModels.add(chatModel);
|
this.chatModels.add(chatModel);
|
||||||
@@ -622,10 +652,28 @@
|
|||||||
this.settingsModel = settingsModel2;
|
this.settingsModel = settingsModel2;
|
||||||
this.connectionModel = connectionModel2;
|
this.connectionModel = connectionModel2;
|
||||||
this.loadChats();
|
this.loadChats();
|
||||||
|
connectionModel2.setMessageHandler(this.messageHandler);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// src/ViewModel/chatViewModel.tsx
|
// src/Model/chatMessageViewModel.ts
|
||||||
|
var ChatMessageViewModel = class {
|
||||||
|
chatModel;
|
||||||
|
// data
|
||||||
|
channel;
|
||||||
|
sender;
|
||||||
|
dateSent;
|
||||||
|
body = new State("");
|
||||||
|
constructor(chatModel, chatMessage) {
|
||||||
|
this.chatModel = chatModel;
|
||||||
|
this.channel = chatMessage.channel;
|
||||||
|
this.sender = chatMessage.sender;
|
||||||
|
this.dateSent = new Date(chatMessage.dateSent).toLocaleString();
|
||||||
|
this.body.value = chatMessage.body;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// src/ViewModel/chatViewModel.ts
|
||||||
var ChatViewModel = class {
|
var ChatViewModel = class {
|
||||||
chatModel;
|
chatModel;
|
||||||
storageModel;
|
storageModel;
|
||||||
@@ -647,6 +695,7 @@
|
|||||||
selectedPage = new State(
|
selectedPage = new State(
|
||||||
"messages" /* Messages */
|
"messages" /* Messages */
|
||||||
);
|
);
|
||||||
|
chatMessageViewModels = new ListState();
|
||||||
composingMessage = new State("");
|
composingMessage = new State("");
|
||||||
// guards
|
// guards
|
||||||
cannotSetPrimaryChannel = createProxyState(
|
cannotSetPrimaryChannel = createProxyState(
|
||||||
@@ -670,6 +719,11 @@
|
|||||||
close = () => {
|
close = () => {
|
||||||
this.chatListViewModel.closeChat();
|
this.chatListViewModel.closeChat();
|
||||||
};
|
};
|
||||||
|
// add
|
||||||
|
addChatMessage = (chatMessage) => {
|
||||||
|
const chatMessageModel = new ChatMessageViewModel(this.chatModel, chatMessage);
|
||||||
|
this.chatMessageViewModels.add(chatMessageModel);
|
||||||
|
};
|
||||||
// settings
|
// settings
|
||||||
setPrimaryChannel = () => {
|
setPrimaryChannel = () => {
|
||||||
this.chatModel.setPrimaryChannel(this.primaryChannelInput.value);
|
this.chatModel.setPrimaryChannel(this.primaryChannelInput.value);
|
||||||
@@ -734,6 +788,9 @@
|
|||||||
this.storageModel = storageModel2;
|
this.storageModel = storageModel2;
|
||||||
this.settingsViewModel = settingsViewModel2;
|
this.settingsViewModel = settingsViewModel2;
|
||||||
this.chatListViewModel = chatListViewModel2;
|
this.chatListViewModel = chatListViewModel2;
|
||||||
|
chatModel.setMessageHandler((chatMessage) => {
|
||||||
|
this.addChatMessage(chatMessage);
|
||||||
|
});
|
||||||
this.primaryChannel.value = chatModel.info.primaryChannel;
|
this.primaryChannel.value = chatModel.info.primaryChannel;
|
||||||
this.primaryChannelInput.value = chatModel.info.primaryChannel;
|
this.primaryChannelInput.value = chatModel.info.primaryChannel;
|
||||||
this.encryptionKeyInput.value = chatModel.info.encryptionKey;
|
this.encryptionKeyInput.value = chatModel.info.encryptionKey;
|
||||||
@@ -741,18 +798,21 @@
|
|||||||
[this.encryptionKeyInput],
|
[this.encryptionKeyInput],
|
||||||
() => this.encryptionKeyInput.value == this.chatModel.info.encryptionKey
|
() => this.encryptionKeyInput.value == this.chatModel.info.encryptionKey
|
||||||
);
|
);
|
||||||
this.cannotSendMessage = createProxyState(
|
|
||||||
[this.settingsViewModel.username, this.composingMessage],
|
|
||||||
() => this.settingsViewModel.username.value == "" || this.composingMessage.value == ""
|
|
||||||
);
|
|
||||||
this.color.value = chatModel.color;
|
this.color.value = chatModel.color;
|
||||||
this.restoreSecondaryChannels();
|
this.restoreSecondaryChannels();
|
||||||
this.restorePageSelection();
|
this.restorePageSelection();
|
||||||
this.updateIndex();
|
this.updateIndex();
|
||||||
|
for (const chatMessage of chatModel.messages) {
|
||||||
|
this.addChatMessage(chatMessage);
|
||||||
|
}
|
||||||
|
this.cannotSendMessage = createProxyState(
|
||||||
|
[this.settingsViewModel.username, this.composingMessage],
|
||||||
|
() => this.settingsViewModel.username.value == "" || this.composingMessage.value == ""
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// src/ViewModel/chatListViewModel.tsx
|
// src/ViewModel/chatListViewModel.ts
|
||||||
var ChatListViewModel = class {
|
var ChatListViewModel = class {
|
||||||
chatListModel;
|
chatListModel;
|
||||||
storageModel;
|
storageModel;
|
||||||
@@ -839,6 +899,15 @@
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// src/View/Components/chatMessage.tsx
|
||||||
|
function ChatMessage(chatMessageViewModel) {
|
||||||
|
return /* @__PURE__ */ createElement("div", { class: "tile flex-no" }, /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("span", null, chatMessageViewModel.sender), /* @__PURE__ */ createElement("b", { "subscribe:innerText": chatMessageViewModel.body }), /* @__PURE__ */ createElement("span", null, chatMessageViewModel.dateSent)));
|
||||||
|
}
|
||||||
|
var ChatMessageViewModelToView = (chatMessageViewModel) => {
|
||||||
|
console.log(chatMessageViewModel);
|
||||||
|
return ChatMessage(chatMessageViewModel);
|
||||||
|
};
|
||||||
|
|
||||||
// src/View/translations.ts
|
// src/View/translations.ts
|
||||||
var englishTranslations = {
|
var englishTranslations = {
|
||||||
general: {
|
general: {
|
||||||
@@ -928,7 +997,28 @@
|
|||||||
|
|
||||||
// src/View/ChatPages/messagePage.tsx
|
// src/View/ChatPages/messagePage.tsx
|
||||||
function MessagePage(chatViewModel) {
|
function MessagePage(chatViewModel) {
|
||||||
return /* @__PURE__ */ createElement("div", { id: "message-page" }, /* @__PURE__ */ createElement("div", { class: "toolbar" }, /* @__PURE__ */ createElement("span", { "subscribe:innerText": chatViewModel.primaryChannel })), /* @__PURE__ */ createElement("div", { class: "content" }, /* @__PURE__ */ createElement("div", { id: "message-container" }), /* @__PURE__ */ createElement("div", { id: "composer" }, /* @__PURE__ */ createElement(
|
const messageContainer = /* @__PURE__ */ createElement(
|
||||||
|
"div",
|
||||||
|
{
|
||||||
|
id: "message-container",
|
||||||
|
"children:append": [
|
||||||
|
chatViewModel.chatMessageViewModels,
|
||||||
|
ChatMessageViewModelToView
|
||||||
|
]
|
||||||
|
}
|
||||||
|
);
|
||||||
|
function scrollDown() {
|
||||||
|
console.log("down");
|
||||||
|
messageContainer.scrollTop = messageContainer.scrollHeight;
|
||||||
|
}
|
||||||
|
function scrollDownIfApplicable() {
|
||||||
|
const scrollFromBottom = messageContainer.scrollHeight - (messageContainer.scrollTop + messageContainer.offsetHeight);
|
||||||
|
if (scrollFromBottom > 400) return;
|
||||||
|
scrollDown();
|
||||||
|
}
|
||||||
|
chatViewModel.chatMessageViewModels.subscribeSilent(scrollDownIfApplicable);
|
||||||
|
setTimeout(() => scrollDown(), 100);
|
||||||
|
return /* @__PURE__ */ createElement("div", { id: "message-page" }, /* @__PURE__ */ createElement("div", { class: "toolbar" }, /* @__PURE__ */ createElement("span", { "subscribe:innerText": chatViewModel.primaryChannel })), /* @__PURE__ */ createElement("div", { class: "content" }, messageContainer, /* @__PURE__ */ createElement("div", { id: "composer" }, /* @__PURE__ */ createElement(
|
||||||
"input",
|
"input",
|
||||||
{
|
{
|
||||||
"bind:value": chatViewModel.composingMessage,
|
"bind:value": chatViewModel.composingMessage,
|
||||||
@@ -1307,6 +1397,7 @@
|
|||||||
};
|
};
|
||||||
messageHandler = () => {
|
messageHandler = () => {
|
||||||
};
|
};
|
||||||
|
channelsToSubscribe = /* @__PURE__ */ new Set();
|
||||||
// connection
|
// connection
|
||||||
connect = (address) => {
|
connect = (address) => {
|
||||||
this.udn.connect(address);
|
this.udn.connect(address);
|
||||||
@@ -1321,11 +1412,21 @@
|
|||||||
console.log("connection status:", this.isConnected, this.address);
|
console.log("connection status:", this.isConnected, this.address);
|
||||||
this.connectionChangeHandler();
|
this.connectionChangeHandler();
|
||||||
if (this.isConnected == false) return;
|
if (this.isConnected == false) return;
|
||||||
if (this.address) {
|
if (!this.address) return;
|
||||||
this.storeAddress(this.address);
|
this.storeAddress(this.address);
|
||||||
}
|
this.sendSubscriptionRequest();
|
||||||
this.sendMessagesInOutbox();
|
this.sendMessagesInOutbox();
|
||||||
};
|
};
|
||||||
|
// subscription
|
||||||
|
addChannel = (channel) => {
|
||||||
|
this.channelsToSubscribe.add(channel);
|
||||||
|
this.sendSubscriptionRequest();
|
||||||
|
};
|
||||||
|
sendSubscriptionRequest = () => {
|
||||||
|
for (const channel of this.channelsToSubscribe) {
|
||||||
|
this.udn.subscribe(channel);
|
||||||
|
}
|
||||||
|
};
|
||||||
// outbox
|
// outbox
|
||||||
getOutboxMessags = () => {
|
getOutboxMessags = () => {
|
||||||
const outboxPath = storageKeys.outbox;
|
const outboxPath = storageKeys.outbox;
|
||||||
@@ -1408,7 +1509,7 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// src/ViewModel/connectionViewModel.tsx
|
// src/ViewModel/connectionViewModel.ts
|
||||||
var ConnectionViewModel = class {
|
var ConnectionViewModel = class {
|
||||||
connectionModel;
|
connectionModel;
|
||||||
// state
|
// state
|
||||||
@@ -1439,8 +1540,6 @@
|
|||||||
this.previousAddresses.add(this.connectionModel.address);
|
this.previousAddresses.add(this.connectionModel.address);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
messageHandler = (data) => {
|
|
||||||
};
|
|
||||||
// methods
|
// methods
|
||||||
connect = () => {
|
connect = () => {
|
||||||
this.connectToAddress(this.serverAddressInput.value);
|
this.connectToAddress(this.serverAddressInput.value);
|
||||||
@@ -1471,7 +1570,6 @@
|
|||||||
this.connectionModel = connectionModel2;
|
this.connectionModel = connectionModel2;
|
||||||
this.updatePreviousAddresses();
|
this.updatePreviousAddresses();
|
||||||
connectionModel2.setConnectionChangeHandler(this.connectionChangeHandler);
|
connectionModel2.setConnectionChangeHandler(this.connectionChangeHandler);
|
||||||
connectionModel2.setMessageHandler(this.messageHandler);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import StorageModel, { storageKeys } from "./storageModel";
|
|||||||
|
|
||||||
import { ChatModel } from "./chatModel";
|
import { ChatModel } from "./chatModel";
|
||||||
import ConnectionModel from "./connectionModel";
|
import ConnectionModel from "./connectionModel";
|
||||||
|
import { Message } from "udn-frontend";
|
||||||
import SettingsModel from "./settingsModel";
|
import SettingsModel from "./settingsModel";
|
||||||
import { localeCompare } from "./Utility/utility";
|
import { localeCompare } from "./Utility/utility";
|
||||||
import { v4 } from "uuid";
|
import { v4 } from "uuid";
|
||||||
@@ -15,6 +16,23 @@ export default class ChatListModel {
|
|||||||
chatModels = new Set<ChatModel>();
|
chatModels = new Set<ChatModel>();
|
||||||
sortedPrimaryChannels: string[] = [];
|
sortedPrimaryChannels: string[] = [];
|
||||||
|
|
||||||
|
// handlers
|
||||||
|
messageHandler = (data: Message): void => {
|
||||||
|
for (const chatModel of this.chatModels) {
|
||||||
|
const channel: string | undefined = data.messageChannel;
|
||||||
|
const body: string | undefined = data.messageBody;
|
||||||
|
if (channel == undefined) return;
|
||||||
|
if (body == undefined) return;
|
||||||
|
|
||||||
|
const allChannels = channel.split("/");
|
||||||
|
for (const channel of allChannels) {
|
||||||
|
if (channel != chatModel.info.primaryChannel) continue;
|
||||||
|
chatModel.handleMessage(channel, body);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// store & add
|
// store & add
|
||||||
addChatModel = (chatModel: ChatModel) => {
|
addChatModel = (chatModel: ChatModel) => {
|
||||||
this.chatModels.add(chatModel);
|
this.chatModels.add(chatModel);
|
||||||
@@ -57,7 +75,7 @@ export default class ChatListModel {
|
|||||||
getIndexOfPrimaryChannel(primaryChannel: string): number {
|
getIndexOfPrimaryChannel(primaryChannel: string): number {
|
||||||
return this.sortedPrimaryChannels.indexOf(primaryChannel);
|
return this.sortedPrimaryChannels.indexOf(primaryChannel);
|
||||||
}
|
}
|
||||||
|
|
||||||
// restore
|
// restore
|
||||||
loadChats = (): void => {
|
loadChats = (): void => {
|
||||||
const chatDir = storageKeys.chats;
|
const chatDir = storageKeys.chats;
|
||||||
@@ -75,10 +93,16 @@ export default class ChatListModel {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// init
|
// init
|
||||||
constructor(storageModel: StorageModel, settingsModel: SettingsModel, connectionModel: ConnectionModel) {
|
constructor(
|
||||||
|
storageModel: StorageModel,
|
||||||
|
settingsModel: SettingsModel,
|
||||||
|
connectionModel: ConnectionModel
|
||||||
|
) {
|
||||||
this.storageModel = storageModel;
|
this.storageModel = storageModel;
|
||||||
this.settingsModel = settingsModel;
|
this.settingsModel = settingsModel;
|
||||||
this.connectionModel = connectionModel;
|
this.connectionModel = connectionModel;
|
||||||
this.loadChats();
|
this.loadChats();
|
||||||
|
|
||||||
|
connectionModel.setMessageHandler(this.messageHandler);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
// this file is responsible for managing individual chat messages.
|
||||||
|
|
||||||
|
import * as React from "bloatless-react";
|
||||||
|
|
||||||
|
import { ChatMessage, ChatModel } from "./chatModel";
|
||||||
|
|
||||||
|
export default class ChatMessageViewModel {
|
||||||
|
chatModel: ChatModel;
|
||||||
|
|
||||||
|
// data
|
||||||
|
channel: string;
|
||||||
|
sender: string;
|
||||||
|
dateSent: string;
|
||||||
|
body: React.State<string> = new React.State("");
|
||||||
|
|
||||||
|
constructor(chatModel: ChatModel, chatMessage: ChatMessage) {
|
||||||
|
this.chatModel = chatModel;
|
||||||
|
|
||||||
|
this.channel = chatMessage.channel;
|
||||||
|
this.sender = chatMessage.sender;
|
||||||
|
this.dateSent = new Date(chatMessage.dateSent).toLocaleString();
|
||||||
|
this.body.value = chatMessage.body;
|
||||||
|
}
|
||||||
|
}
|
||||||
+38
-8
@@ -1,6 +1,7 @@
|
|||||||
// this file is responsible for managing chats and chat messages. objects are delegated to objectModel.ts.
|
// this file is responsible for managing chats. objects are delegated to objectModel.ts.
|
||||||
|
|
||||||
import StorageModel, { storageKeys } from "./storageModel";
|
import StorageModel, { storageKeys } from "./storageModel";
|
||||||
|
import { createTimestamp, parse } from "./Utility/utility";
|
||||||
|
|
||||||
import ChatListModel from "./chatListModel";
|
import ChatListModel from "./chatListModel";
|
||||||
import { Color } from "../ViewModel/colors";
|
import { Color } from "../ViewModel/colors";
|
||||||
@@ -8,7 +9,6 @@ import ConnectionModel from "./connectionModel";
|
|||||||
import { Message } from "udn-frontend";
|
import { Message } from "udn-frontend";
|
||||||
import SettingsModel from "./settingsModel";
|
import SettingsModel from "./settingsModel";
|
||||||
import { checkIsValidObject } from "./Utility/typeSafety";
|
import { checkIsValidObject } from "./Utility/typeSafety";
|
||||||
import { createTimestamp } from "./Utility/utility";
|
|
||||||
import { v4 } from "uuid";
|
import { v4 } from "uuid";
|
||||||
|
|
||||||
export class ChatModel {
|
export class ChatModel {
|
||||||
@@ -22,6 +22,8 @@ export class ChatModel {
|
|||||||
info: ChatInfo;
|
info: ChatInfo;
|
||||||
color: Color;
|
color: Color;
|
||||||
|
|
||||||
|
chatMessageHandler: (chatMessage: ChatMessage) => void = () => {};
|
||||||
|
|
||||||
// sorting
|
// sorting
|
||||||
get index(): number {
|
get index(): number {
|
||||||
return this.chatListModel.getIndexOfPrimaryChannel(
|
return this.chatListModel.getIndexOfPrimaryChannel(
|
||||||
@@ -59,6 +61,7 @@ export class ChatModel {
|
|||||||
this.info.primaryChannel = primaryChannel;
|
this.info.primaryChannel = primaryChannel;
|
||||||
this.storeInfo();
|
this.storeInfo();
|
||||||
this.chatListModel.updateIndices();
|
this.chatListModel.updateIndices();
|
||||||
|
this.subscribe();
|
||||||
};
|
};
|
||||||
|
|
||||||
setSecondaryChannels = (secondaryChannels: string[]): void => {
|
setSecondaryChannels = (secondaryChannels: string[]): void => {
|
||||||
@@ -85,14 +88,15 @@ export class ChatModel {
|
|||||||
this.storageModel.store(this.colorPath, this.color);
|
this.storageModel.store(this.colorPath, this.color);
|
||||||
};
|
};
|
||||||
|
|
||||||
addMessage = (message: ChatMessage): void => {
|
addMessage = (chatMessage: ChatMessage): void => {
|
||||||
const messagePath: string[] = this.getMessagePath(message.id);
|
const messagePath: string[] = this.getMessagePath(chatMessage.id);
|
||||||
this.storageModel.storeStringifiable(messagePath, message);
|
this.storageModel.storeStringifiable(messagePath, chatMessage);
|
||||||
|
this.chatMessageHandler(chatMessage);
|
||||||
};
|
};
|
||||||
|
|
||||||
addObject = (object: ChatObject): void => {
|
addObject = (chatObject: ChatObject): void => {
|
||||||
const objectPath: string[] = this.getObjectPath(object.id);
|
const objectPath: string[] = this.getObjectPath(chatObject.id);
|
||||||
this.storageModel.storeStringifiable(objectPath, object);
|
this.storageModel.storeStringifiable(objectPath, chatObject);
|
||||||
};
|
};
|
||||||
|
|
||||||
// delete
|
// delete
|
||||||
@@ -179,6 +183,21 @@ export class ChatModel {
|
|||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
handleMessage = (channel: string, body: string): void => {
|
||||||
|
const chatMessage: ChatMessage | null = ChatModel.parseMessage(body);
|
||||||
|
if (chatMessage == null) return;
|
||||||
|
|
||||||
|
this.addMessage(chatMessage);
|
||||||
|
};
|
||||||
|
|
||||||
|
setMessageHandler = (handler: (chatMessage: ChatMessage) => void): void => {
|
||||||
|
this.chatMessageHandler = handler;
|
||||||
|
};
|
||||||
|
|
||||||
|
subscribe = (): void => {
|
||||||
|
this.connectionModel.addChannel(this.info.primaryChannel);
|
||||||
|
};
|
||||||
|
|
||||||
// init
|
// init
|
||||||
constructor(
|
constructor(
|
||||||
storageModel: StorageModel,
|
storageModel: StorageModel,
|
||||||
@@ -195,6 +214,7 @@ export class ChatModel {
|
|||||||
|
|
||||||
this.restoreInfo();
|
this.restoreInfo();
|
||||||
this.restoreColor();
|
this.restoreColor();
|
||||||
|
this.subscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
// utility
|
// utility
|
||||||
@@ -223,6 +243,16 @@ export class ChatModel {
|
|||||||
dateSent: createTimestamp(),
|
dateSent: createTimestamp(),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
static parseMessage = (string: string): ChatMessage | null => {
|
||||||
|
try {
|
||||||
|
const parsed: any = parse(string);
|
||||||
|
if (checkIsValidObject(parsed) == false) return null;
|
||||||
|
return parsed;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// types
|
// types
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ export default class ConnectionModel {
|
|||||||
connectionChangeHandler: () => void = () => {};
|
connectionChangeHandler: () => void = () => {};
|
||||||
messageHandler: (data: Message) => void = () => {};
|
messageHandler: (data: Message) => void = () => {};
|
||||||
|
|
||||||
|
channelsToSubscribe: Set<string> = new Set();
|
||||||
|
|
||||||
// connection
|
// connection
|
||||||
connect = (address: string): void => {
|
connect = (address: string): void => {
|
||||||
this.udn.connect(address);
|
this.udn.connect(address);
|
||||||
@@ -39,16 +41,26 @@ export default class ConnectionModel {
|
|||||||
handleConnectionChange = (): void => {
|
handleConnectionChange = (): void => {
|
||||||
console.log("connection status:", this.isConnected, this.address);
|
console.log("connection status:", this.isConnected, this.address);
|
||||||
this.connectionChangeHandler();
|
this.connectionChangeHandler();
|
||||||
|
|
||||||
if (this.isConnected == false) return;
|
if (this.isConnected == false) return;
|
||||||
|
if (!this.address) return;
|
||||||
|
|
||||||
if (this.address) {
|
this.storeAddress(this.address);
|
||||||
this.storeAddress(this.address);
|
this.sendSubscriptionRequest();
|
||||||
}
|
|
||||||
|
|
||||||
this.sendMessagesInOutbox();
|
this.sendMessagesInOutbox();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// subscription
|
||||||
|
addChannel = (channel: string): void => {
|
||||||
|
this.channelsToSubscribe.add(channel);
|
||||||
|
this.sendSubscriptionRequest();
|
||||||
|
};
|
||||||
|
|
||||||
|
sendSubscriptionRequest = (): void => {
|
||||||
|
for (const channel of this.channelsToSubscribe) {
|
||||||
|
this.udn.subscribe(channel);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// outbox
|
// outbox
|
||||||
getOutboxMessags = (): ChatMessage[] => {
|
getOutboxMessags = (): ChatMessage[] => {
|
||||||
const outboxPath: string[] = storageKeys.outbox;
|
const outboxPath: string[] = storageKeys.outbox;
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
// this file is responsible for managing message objects.
|
// this file is responsible for managing message objects.c
|
||||||
@@ -1,29 +1,39 @@
|
|||||||
/* layout */
|
/* layout */
|
||||||
#chat-page #main #message-page .content {
|
#chat-page #main #message-page .content {
|
||||||
padding: 0px;
|
padding: 0px;
|
||||||
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-rows: 1fr var(--control-height) var(--safe-area-bottom);
|
grid-template-rows: 1fr var(--control-height) var(--safe-area-bottom);
|
||||||
}
|
}
|
||||||
|
|
||||||
#message-page #message-container {
|
#message-page #message-container {
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* composer */
|
/* composer */
|
||||||
#message-page #composer {
|
#message-page #composer {
|
||||||
border-top: 1px solid var(--border);
|
border-top: 1px solid var(--border);
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
#message-page #composer input {
|
#message-page #composer input {
|
||||||
margin: 0px;
|
margin: 0px;
|
||||||
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: unset;
|
max-width: unset;
|
||||||
|
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#message-page #message-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
padding: var(--content-padding);
|
||||||
|
gap: var(--gap-between-tiles);
|
||||||
|
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,17 +2,43 @@ import "./messagePage.css";
|
|||||||
|
|
||||||
import * as React from "bloatless-react";
|
import * as React from "bloatless-react";
|
||||||
|
|
||||||
|
import { ChatMessageViewModelToView } from "../Components/chatMessage";
|
||||||
import ChatViewModel from "../../ViewModel/chatViewModel";
|
import ChatViewModel from "../../ViewModel/chatViewModel";
|
||||||
import { translations } from "../translations";
|
import { translations } from "../translations";
|
||||||
|
|
||||||
export function MessagePage(chatViewModel: ChatViewModel) {
|
export function MessagePage(chatViewModel: ChatViewModel) {
|
||||||
|
const messageContainer = (
|
||||||
|
<div
|
||||||
|
id="message-container"
|
||||||
|
children:append={[
|
||||||
|
chatViewModel.chatMessageViewModels,
|
||||||
|
ChatMessageViewModelToView,
|
||||||
|
]}
|
||||||
|
></div>
|
||||||
|
);
|
||||||
|
|
||||||
|
function scrollDown() {
|
||||||
|
console.log("down");
|
||||||
|
messageContainer.scrollTop = messageContainer.scrollHeight;
|
||||||
|
}
|
||||||
|
function scrollDownIfApplicable() {
|
||||||
|
const scrollFromBottom =
|
||||||
|
messageContainer.scrollHeight -
|
||||||
|
(messageContainer.scrollTop + messageContainer.offsetHeight);
|
||||||
|
if (scrollFromBottom > 400) return;
|
||||||
|
|
||||||
|
scrollDown();
|
||||||
|
}
|
||||||
|
chatViewModel.chatMessageViewModels.subscribeSilent(scrollDownIfApplicable);
|
||||||
|
setTimeout(() => scrollDown(), 100)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div id="message-page">
|
<div id="message-page">
|
||||||
<div class="toolbar">
|
<div class="toolbar">
|
||||||
<span subscribe:innerText={chatViewModel.primaryChannel}></span>
|
<span subscribe:innerText={chatViewModel.primaryChannel}></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<div id="message-container"></div>
|
{messageContainer}
|
||||||
<div id="composer">
|
<div id="composer">
|
||||||
<input
|
<input
|
||||||
bind:value={chatViewModel.composingMessage}
|
bind:value={chatViewModel.composingMessage}
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import * as React from "bloatless-react";
|
||||||
|
|
||||||
|
import ChatMessageViewModel from "../../Model/chatMessageViewModel";
|
||||||
|
|
||||||
|
export function ChatMessage(chatMessageViewModel: ChatMessageViewModel) {
|
||||||
|
return (
|
||||||
|
<div class="tile flex-no">
|
||||||
|
<div>
|
||||||
|
<span>{chatMessageViewModel.sender}</span>
|
||||||
|
<b subscribe:innerText={chatMessageViewModel.body}></b>
|
||||||
|
<span>{chatMessageViewModel.dateSent}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ChatMessageViewModelToView: React.StateItemConverter<
|
||||||
|
ChatMessageViewModel
|
||||||
|
> = (chatMessageViewModel: ChatMessageViewModel) => {
|
||||||
|
console.log(chatMessageViewModel);
|
||||||
|
return ChatMessage(chatMessageViewModel);
|
||||||
|
};
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
import * as React from "bloatless-react";
|
import * as React from "bloatless-react";
|
||||||
|
|
||||||
|
import { ChatMessage, ChatModel } from "../Model/chatModel";
|
||||||
import StorageModel, { storageKeys } from "../Model/storageModel";
|
import StorageModel, { storageKeys } from "../Model/storageModel";
|
||||||
|
|
||||||
import ChatListViewModel from "./chatListViewModel";
|
import ChatListViewModel from "./chatListViewModel";
|
||||||
import { ChatModel } from "../Model/chatModel";
|
import ChatMessageViewModel from "../Model/chatMessageViewModel";
|
||||||
import { Color } from "./colors";
|
import { Color } from "./colors";
|
||||||
import SettingsViewModel from "./settingsViewModel";
|
import SettingsViewModel from "./settingsViewModel";
|
||||||
import { localeCompare } from "../Model/Utility/utility";
|
import { localeCompare } from "../Model/Utility/utility";
|
||||||
@@ -36,6 +37,7 @@ export default class ChatViewModel {
|
|||||||
ChatPageType.Messages
|
ChatPageType.Messages
|
||||||
);
|
);
|
||||||
|
|
||||||
|
chatMessageViewModels: React.ListState<ChatMessageViewModel> = new React.ListState();
|
||||||
composingMessage: React.State<string> = new React.State("");
|
composingMessage: React.State<string> = new React.State("");
|
||||||
|
|
||||||
// guards
|
// guards
|
||||||
@@ -47,8 +49,7 @@ export default class ChatViewModel {
|
|||||||
);
|
);
|
||||||
cannotAddSecondaryChannel: React.State<boolean> = React.createProxyState(
|
cannotAddSecondaryChannel: React.State<boolean> = React.createProxyState(
|
||||||
[this.newSecondaryChannelInput],
|
[this.newSecondaryChannelInput],
|
||||||
() =>
|
() => this.newSecondaryChannelInput.value == ""
|
||||||
this.newSecondaryChannelInput.value == ""
|
|
||||||
);
|
);
|
||||||
cannotSetEncryptionKey: React.State<boolean>;
|
cannotSetEncryptionKey: React.State<boolean>;
|
||||||
cannotSendMessage: React.State<boolean>;
|
cannotSendMessage: React.State<boolean>;
|
||||||
@@ -67,6 +68,12 @@ export default class ChatViewModel {
|
|||||||
this.chatListViewModel.closeChat();
|
this.chatListViewModel.closeChat();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// add
|
||||||
|
addChatMessage = (chatMessage: ChatMessage): void => {
|
||||||
|
const chatMessageModel = new ChatMessageViewModel(this.chatModel, chatMessage);
|
||||||
|
this.chatMessageViewModels.add(chatMessageModel);
|
||||||
|
};
|
||||||
|
|
||||||
// settings
|
// settings
|
||||||
setPrimaryChannel = (): void => {
|
setPrimaryChannel = (): void => {
|
||||||
this.chatModel.setPrimaryChannel(this.primaryChannelInput.value);
|
this.chatModel.setPrimaryChannel(this.primaryChannelInput.value);
|
||||||
@@ -145,11 +152,18 @@ export default class ChatViewModel {
|
|||||||
settingsViewModel: SettingsViewModel,
|
settingsViewModel: SettingsViewModel,
|
||||||
chatListViewModel: ChatListViewModel
|
chatListViewModel: ChatListViewModel
|
||||||
) {
|
) {
|
||||||
|
// models
|
||||||
this.chatModel = chatModel;
|
this.chatModel = chatModel;
|
||||||
this.storageModel = storageModel;
|
this.storageModel = storageModel;
|
||||||
this.settingsViewModel = settingsViewModel;
|
this.settingsViewModel = settingsViewModel;
|
||||||
this.chatListViewModel = chatListViewModel;
|
this.chatListViewModel = chatListViewModel;
|
||||||
|
|
||||||
|
// handlers
|
||||||
|
chatModel.setMessageHandler((chatMessage) => {
|
||||||
|
this.addChatMessage(chatMessage);
|
||||||
|
});
|
||||||
|
|
||||||
|
// restore
|
||||||
this.primaryChannel.value = chatModel.info.primaryChannel;
|
this.primaryChannel.value = chatModel.info.primaryChannel;
|
||||||
this.primaryChannelInput.value = chatModel.info.primaryChannel;
|
this.primaryChannelInput.value = chatModel.info.primaryChannel;
|
||||||
|
|
||||||
@@ -159,18 +173,23 @@ export default class ChatViewModel {
|
|||||||
() => this.encryptionKeyInput.value == this.chatModel.info.encryptionKey
|
() => this.encryptionKeyInput.value == this.chatModel.info.encryptionKey
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.color.value = chatModel.color;
|
||||||
|
|
||||||
|
this.restoreSecondaryChannels();
|
||||||
|
this.restorePageSelection();
|
||||||
|
this.updateIndex();
|
||||||
|
|
||||||
|
for (const chatMessage of chatModel.messages) {
|
||||||
|
this.addChatMessage(chatMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
// guards
|
||||||
this.cannotSendMessage = React.createProxyState(
|
this.cannotSendMessage = React.createProxyState(
|
||||||
[this.settingsViewModel.username, this.composingMessage],
|
[this.settingsViewModel.username, this.composingMessage],
|
||||||
() =>
|
() =>
|
||||||
this.settingsViewModel.username.value == "" ||
|
this.settingsViewModel.username.value == "" ||
|
||||||
this.composingMessage.value == ""
|
this.composingMessage.value == ""
|
||||||
);
|
);
|
||||||
|
|
||||||
this.color.value = chatModel.color;
|
|
||||||
|
|
||||||
this.restoreSecondaryChannels();
|
|
||||||
this.restorePageSelection();
|
|
||||||
this.updateIndex();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user