add basic thread view

This commit is contained in:
marlon-erler
2024-07-21 01:12:35 +02:00
parent 611b876509
commit c97b38dcd6
13 changed files with 359 additions and 87 deletions
+7
View File
@@ -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 {
+147 -49
View File
@@ -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);
} }
}; };
+25 -1
View File
@@ -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);
@@ -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);
} }
} }
+24
View File
@@ -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
View File
@@ -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
+17 -5
View File
@@ -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
View File
@@ -1 +1 @@
// this file is responsible for managing message objects. // this file is responsible for managing message objects.c
+21 -11
View File
@@ -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;
} }
+27 -1
View File
@@ -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}
+22
View File
@@ -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();
} }
} }