add basic thread view
This commit is contained in:
@@ -2,6 +2,7 @@ import StorageModel, { storageKeys } from "./storageModel";
|
||||
|
||||
import { ChatModel } from "./chatModel";
|
||||
import ConnectionModel from "./connectionModel";
|
||||
import { Message } from "udn-frontend";
|
||||
import SettingsModel from "./settingsModel";
|
||||
import { localeCompare } from "./Utility/utility";
|
||||
import { v4 } from "uuid";
|
||||
@@ -15,6 +16,23 @@ export default class ChatListModel {
|
||||
chatModels = new Set<ChatModel>();
|
||||
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
|
||||
addChatModel = (chatModel: ChatModel) => {
|
||||
this.chatModels.add(chatModel);
|
||||
@@ -57,7 +75,7 @@ export default class ChatListModel {
|
||||
getIndexOfPrimaryChannel(primaryChannel: string): number {
|
||||
return this.sortedPrimaryChannels.indexOf(primaryChannel);
|
||||
}
|
||||
|
||||
|
||||
// restore
|
||||
loadChats = (): void => {
|
||||
const chatDir = storageKeys.chats;
|
||||
@@ -75,10 +93,16 @@ export default class ChatListModel {
|
||||
};
|
||||
|
||||
// init
|
||||
constructor(storageModel: StorageModel, settingsModel: SettingsModel, connectionModel: ConnectionModel) {
|
||||
constructor(
|
||||
storageModel: StorageModel,
|
||||
settingsModel: SettingsModel,
|
||||
connectionModel: ConnectionModel
|
||||
) {
|
||||
this.storageModel = storageModel;
|
||||
this.settingsModel = settingsModel;
|
||||
this.connectionModel = connectionModel;
|
||||
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 { createTimestamp, parse } from "./Utility/utility";
|
||||
|
||||
import ChatListModel from "./chatListModel";
|
||||
import { Color } from "../ViewModel/colors";
|
||||
@@ -8,7 +9,6 @@ import ConnectionModel from "./connectionModel";
|
||||
import { Message } from "udn-frontend";
|
||||
import SettingsModel from "./settingsModel";
|
||||
import { checkIsValidObject } from "./Utility/typeSafety";
|
||||
import { createTimestamp } from "./Utility/utility";
|
||||
import { v4 } from "uuid";
|
||||
|
||||
export class ChatModel {
|
||||
@@ -22,6 +22,8 @@ export class ChatModel {
|
||||
info: ChatInfo;
|
||||
color: Color;
|
||||
|
||||
chatMessageHandler: (chatMessage: ChatMessage) => void = () => {};
|
||||
|
||||
// sorting
|
||||
get index(): number {
|
||||
return this.chatListModel.getIndexOfPrimaryChannel(
|
||||
@@ -59,6 +61,7 @@ export class ChatModel {
|
||||
this.info.primaryChannel = primaryChannel;
|
||||
this.storeInfo();
|
||||
this.chatListModel.updateIndices();
|
||||
this.subscribe();
|
||||
};
|
||||
|
||||
setSecondaryChannels = (secondaryChannels: string[]): void => {
|
||||
@@ -85,14 +88,15 @@ export class ChatModel {
|
||||
this.storageModel.store(this.colorPath, this.color);
|
||||
};
|
||||
|
||||
addMessage = (message: ChatMessage): void => {
|
||||
const messagePath: string[] = this.getMessagePath(message.id);
|
||||
this.storageModel.storeStringifiable(messagePath, message);
|
||||
addMessage = (chatMessage: ChatMessage): void => {
|
||||
const messagePath: string[] = this.getMessagePath(chatMessage.id);
|
||||
this.storageModel.storeStringifiable(messagePath, chatMessage);
|
||||
this.chatMessageHandler(chatMessage);
|
||||
};
|
||||
|
||||
addObject = (object: ChatObject): void => {
|
||||
const objectPath: string[] = this.getObjectPath(object.id);
|
||||
this.storageModel.storeStringifiable(objectPath, object);
|
||||
addObject = (chatObject: ChatObject): void => {
|
||||
const objectPath: string[] = this.getObjectPath(chatObject.id);
|
||||
this.storageModel.storeStringifiable(objectPath, chatObject);
|
||||
};
|
||||
|
||||
// delete
|
||||
@@ -179,6 +183,21 @@ export class ChatModel {
|
||||
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
|
||||
constructor(
|
||||
storageModel: StorageModel,
|
||||
@@ -195,6 +214,7 @@ export class ChatModel {
|
||||
|
||||
this.restoreInfo();
|
||||
this.restoreColor();
|
||||
this.subscribe();
|
||||
}
|
||||
|
||||
// utility
|
||||
@@ -223,6 +243,16 @@ export class ChatModel {
|
||||
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
|
||||
|
||||
@@ -23,6 +23,8 @@ export default class ConnectionModel {
|
||||
connectionChangeHandler: () => void = () => {};
|
||||
messageHandler: (data: Message) => void = () => {};
|
||||
|
||||
channelsToSubscribe: Set<string> = new Set();
|
||||
|
||||
// connection
|
||||
connect = (address: string): void => {
|
||||
this.udn.connect(address);
|
||||
@@ -39,16 +41,26 @@ export default class ConnectionModel {
|
||||
handleConnectionChange = (): void => {
|
||||
console.log("connection status:", this.isConnected, this.address);
|
||||
this.connectionChangeHandler();
|
||||
|
||||
if (this.isConnected == false) return;
|
||||
if (!this.address) return;
|
||||
|
||||
if (this.address) {
|
||||
this.storeAddress(this.address);
|
||||
}
|
||||
|
||||
this.storeAddress(this.address);
|
||||
this.sendSubscriptionRequest();
|
||||
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
|
||||
getOutboxMessags = (): ChatMessage[] => {
|
||||
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 */
|
||||
#chat-page #main #message-page .content {
|
||||
padding: 0px;
|
||||
padding: 0px;
|
||||
|
||||
overflow: hidden;
|
||||
overflow: hidden;
|
||||
|
||||
display: grid;
|
||||
grid-template-rows: 1fr var(--control-height) var(--safe-area-bottom);
|
||||
display: grid;
|
||||
grid-template-rows: 1fr var(--control-height) var(--safe-area-bottom);
|
||||
}
|
||||
|
||||
#message-page #message-container {
|
||||
overflow: auto;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/* composer */
|
||||
#message-page #composer {
|
||||
border-top: 1px solid var(--border);
|
||||
border-top: 1px solid var(--border);
|
||||
|
||||
display: flex;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
#message-page #composer input {
|
||||
margin: 0px;
|
||||
margin: 0px;
|
||||
|
||||
width: 100%;
|
||||
max-width: unset;
|
||||
width: 100%;
|
||||
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 { ChatMessageViewModelToView } from "../Components/chatMessage";
|
||||
import ChatViewModel from "../../ViewModel/chatViewModel";
|
||||
import { translations } from "../translations";
|
||||
|
||||
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 (
|
||||
<div id="message-page">
|
||||
<div class="toolbar">
|
||||
<span subscribe:innerText={chatViewModel.primaryChannel}></span>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div id="message-container"></div>
|
||||
{messageContainer}
|
||||
<div id="composer">
|
||||
<input
|
||||
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 { ChatMessage, ChatModel } from "../Model/chatModel";
|
||||
import StorageModel, { storageKeys } from "../Model/storageModel";
|
||||
|
||||
import ChatListViewModel from "./chatListViewModel";
|
||||
import { ChatModel } from "../Model/chatModel";
|
||||
import ChatMessageViewModel from "../Model/chatMessageViewModel";
|
||||
import { Color } from "./colors";
|
||||
import SettingsViewModel from "./settingsViewModel";
|
||||
import { localeCompare } from "../Model/Utility/utility";
|
||||
@@ -36,6 +37,7 @@ export default class ChatViewModel {
|
||||
ChatPageType.Messages
|
||||
);
|
||||
|
||||
chatMessageViewModels: React.ListState<ChatMessageViewModel> = new React.ListState();
|
||||
composingMessage: React.State<string> = new React.State("");
|
||||
|
||||
// guards
|
||||
@@ -47,8 +49,7 @@ export default class ChatViewModel {
|
||||
);
|
||||
cannotAddSecondaryChannel: React.State<boolean> = React.createProxyState(
|
||||
[this.newSecondaryChannelInput],
|
||||
() =>
|
||||
this.newSecondaryChannelInput.value == ""
|
||||
() => this.newSecondaryChannelInput.value == ""
|
||||
);
|
||||
cannotSetEncryptionKey: React.State<boolean>;
|
||||
cannotSendMessage: React.State<boolean>;
|
||||
@@ -67,6 +68,12 @@ export default class ChatViewModel {
|
||||
this.chatListViewModel.closeChat();
|
||||
};
|
||||
|
||||
// add
|
||||
addChatMessage = (chatMessage: ChatMessage): void => {
|
||||
const chatMessageModel = new ChatMessageViewModel(this.chatModel, chatMessage);
|
||||
this.chatMessageViewModels.add(chatMessageModel);
|
||||
};
|
||||
|
||||
// settings
|
||||
setPrimaryChannel = (): void => {
|
||||
this.chatModel.setPrimaryChannel(this.primaryChannelInput.value);
|
||||
@@ -145,11 +152,18 @@ export default class ChatViewModel {
|
||||
settingsViewModel: SettingsViewModel,
|
||||
chatListViewModel: ChatListViewModel
|
||||
) {
|
||||
// models
|
||||
this.chatModel = chatModel;
|
||||
this.storageModel = storageModel;
|
||||
this.settingsViewModel = settingsViewModel;
|
||||
this.chatListViewModel = chatListViewModel;
|
||||
|
||||
// handlers
|
||||
chatModel.setMessageHandler((chatMessage) => {
|
||||
this.addChatMessage(chatMessage);
|
||||
});
|
||||
|
||||
// restore
|
||||
this.primaryChannel.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.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.settingsViewModel.username, this.composingMessage],
|
||||
() =>
|
||||
this.settingsViewModel.username.value == "" ||
|
||||
this.composingMessage.value == ""
|
||||
);
|
||||
|
||||
this.color.value = chatModel.color;
|
||||
|
||||
this.restoreSecondaryChannels();
|
||||
this.restorePageSelection();
|
||||
this.updateIndex();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user