add message notifications

This commit is contained in:
marlon-erler
2026-04-01 19:00:16 +00:00
parent 8a997aab24
commit 3bd0287517
11 changed files with 256 additions and 18 deletions
+27 -2
View File
@@ -340,12 +340,37 @@ body[showing-chat] #chat-page-wrapper {
justify-content: space-between; justify-content: space-between;
} }
#chat-page > div > #ribbon > span { #chat-page > div > #ribbon > span {
height: 100%;
flex: 1;
overflow-y: auto; overflow-y: auto;
flex-direction: column; flex-direction: column;
justify-content: end; justify-content: end;
} }
#chat-page > div > #ribbon > span.marquee {
height: 100%;
overflow: hidden;
position: relative;
}
#chat-page > div > #ribbon > span.marquee > span {
transform: rotate(270deg);
transform-origin: left;
position: absolute;
bottom: -10px;
left: calc(var(--control-height) / 2);
height: var(--control-height);
min-width: 100vh;
overflow: visible;
text-overflow: visible;
white-space: nowrap;
display: flex;
align-items: center;
gap: var(--gap-between-tiles);
}
#chat-page > div > #ribbon > span.marquee > span > * {
animation: slide-up var(--animation-duration);
}
#chat-page > div > #ribbon > .navigation-buttons {
min-height: calc(var(--control-height) * 4);
flex: 0;
}
#chat-page > div > #ribbon button, #chat-page > div > #ribbon button,
#chat-page > div > #main > div > .pane-wrapper > .pane > .toolbar button { #chat-page > div > #main > div > .pane-wrapper > .pane > .toolbar button {
padding: 0px; padding: 0px;
+76 -5
View File
@@ -651,6 +651,9 @@
], ],
files: (id) => [...filePaths.chat.chatBase(id), "files"] files: (id) => [...filePaths.chat.chatBase(id), "files"]
}, },
notificationModel: {
base: []
},
settingsModel: { settingsModel: {
username: ["user-name"], username: ["user-name"],
firstDayOfWeek: ["first-day-of-week"] firstDayOfWeek: ["first-day-of-week"]
@@ -1373,8 +1376,11 @@
); );
return sorted; return sorted;
} }
static {
// utility // utility
static splitChannel(channelString) {
return channelString.split("/");
}
static {
this.generateChatInfo = (primaryChannel) => { this.generateChatInfo = (primaryChannel) => {
return { return {
dataVersion: DATA_VERSION, dataVersion: DATA_VERSION,
@@ -3024,7 +3030,7 @@
// src/ViewModel/Chat/chatViewModel.ts // src/ViewModel/Chat/chatViewModel.ts
var ChatViewModel = class { var ChatViewModel = class {
// init // init
constructor(coreViewModel, storageModel2, chatModel, settingsViewModel2, connectionViewModel2, chatListViewModel2) { constructor(coreViewModel, storageModel2, chatModel, settingsViewModel2, notificationViewModel, connectionViewModel2, chatListViewModel2) {
this.coreViewModel = coreViewModel; this.coreViewModel = coreViewModel;
// state // state
this.displayedColor = new State("standard" /* Standard */); this.displayedColor = new State("standard" /* Standard */);
@@ -3094,6 +3100,7 @@
this.chatModel = chatModel; this.chatModel = chatModel;
this.settingsViewModel = settingsViewModel2; this.settingsViewModel = settingsViewModel2;
this.connectionViewModel = connectionViewModel2; this.connectionViewModel = connectionViewModel2;
this.notificationViewModel = notificationViewModel;
this.chatListViewModel = chatListViewModel2; this.chatListViewModel = chatListViewModel2;
this.calendarViewModel = new CalendarPageViewModel( this.calendarViewModel = new CalendarPageViewModel(
coreViewModel, coreViewModel,
@@ -3120,6 +3127,7 @@
(chatMessage) => { (chatMessage) => {
this.messagePageViewModel.showChatMessage(chatMessage); this.messagePageViewModel.showChatMessage(chatMessage);
this.updateReadStatus(); this.updateReadStatus();
this.notificationViewModel.showNotification(chatMessage);
} }
); );
this.loadPageSelection(); this.loadPageSelection();
@@ -3128,6 +3136,59 @@
} }
}; };
// src/ViewModel/Global/notificationViewModel.ts
var NotificationViewModel = class _NotificationViewModel {
// init
constructor(chatListViewModel2) {
// data
this.seenMessageIds = /* @__PURE__ */ new Set();
this.messagesInMarquee = [];
this.marquee = new State(void 0);
this.currentIndex = 0;
this.interval = void 0;
// main
this.showNotification = (message) => {
const notification = _NotificationViewModel.createNotification(message);
if (this.seenMessageIds.has(message.id)) return;
const currentChat = this.chatListViewModel.selectedChat.value.chatModel.info.primaryChannel;
if (notification.chat == currentChat) return;
this.messagesInMarquee.push(notification);
this.startLoop();
};
// loop
this.loop = () => {
if (this.messagesInMarquee.length == 0) {
this.marquee.value = void 0;
return this.stopLoop();
}
const notification = this.messagesInMarquee.shift();
this.seenMessageIds.delete(notification.messageId);
this.marquee.value = notification;
};
this.startLoop = () => {
if (this.interval != void 0) return;
this.loop();
this.interval = setInterval(() => {
this.loop();
}, 5e3);
};
this.stopLoop = () => {
clearInterval(this.interval);
this.interval = void 0;
};
this.chatListViewModel = chatListViewModel2;
}
// util
static createNotification(message) {
return {
messageId: message.id,
chat: ChatModel.splitChannel(message.channel)[0],
sender: message.sender,
body: message.body
};
}
};
// src/ViewModel/Chat/chatListViewModel.ts // src/ViewModel/Chat/chatListViewModel.ts
var ChatListViewModel = class { var ChatListViewModel = class {
// init // init
@@ -3171,6 +3232,7 @@
this.storageModel, this.storageModel,
chatModel, chatModel,
this.settingsViewModel, this.settingsViewModel,
this.notificationViewModel,
this.connectionViewModel, this.connectionViewModel,
this this
); );
@@ -3200,6 +3262,7 @@
this.storageModel = storageModel2; this.storageModel = storageModel2;
this.chatListModel = chatListModel2; this.chatListModel = chatListModel2;
this.settingsViewModel = settingsViewModel2; this.settingsViewModel = settingsViewModel2;
this.notificationViewModel = new NotificationViewModel(this);
this.connectionViewModel = connectionViewModel2; this.connectionViewModel = connectionViewModel2;
this.loadChats(); this.loadChats();
} }
@@ -3680,7 +3743,7 @@
} }
// src/View/Components/chatMessage.tsx // src/View/Components/chatMessage.tsx
function ChatMessage2(chatMessageViewModel) { function ChatMessage3(chatMessageViewModel) {
const statusIcon = createProxyState( const statusIcon = createProxyState(
[chatMessageViewModel.status], [chatMessageViewModel.status],
() => { () => {
@@ -3726,7 +3789,7 @@
); );
} }
var ChatMessageViewModelToView = (chatMessageViewModel) => { var ChatMessageViewModelToView = (chatMessageViewModel) => {
return ChatMessage2(chatMessageViewModel); return ChatMessage3(chatMessageViewModel);
}; };
// src/View/ChatPages/messagePage.tsx // src/View/ChatPages/messagePage.tsx
@@ -4555,6 +4618,14 @@
} }
} }
); );
const marqueeContent = createProxyState(
[chatViewModel.notificationViewModel.marquee],
() => {
const value = chatViewModel.notificationViewModel.marquee.value;
if (value == void 0) return /* @__PURE__ */ createElement("span", null);
return /* @__PURE__ */ createElement("span", null, /* @__PURE__ */ createElement("b", null, value.sender), /* @__PURE__ */ createElement("span", { class: "secondary" }, value.chat, ": "), /* @__PURE__ */ createElement("span", null, value.body));
}
);
return /* @__PURE__ */ createElement( return /* @__PURE__ */ createElement(
"article", "article",
{ {
@@ -4579,7 +4650,7 @@
"toggle:hidden": chatViewModel.connectionViewModel.isConnected "toggle:hidden": chatViewModel.connectionViewModel.isConnected
}, },
/* @__PURE__ */ createElement("span", { class: "icon" }, "signal_disconnected") /* @__PURE__ */ createElement("span", { class: "icon" }, "signal_disconnected")
), /* @__PURE__ */ createElement("span", null, ChatViewToggleButton( ), /* @__PURE__ */ createElement("span", { class: "marquee", "children:set": marqueeContent }), /* @__PURE__ */ createElement("span", { class: "navigation-buttons" }, ChatViewToggleButton(
translations.chatPage.pages.calendar, translations.chatPage.pages.calendar,
"calendar_month", "calendar_month",
"calendar" /* Calendar */, "calendar" /* Calendar */,
+4
View File
@@ -271,6 +271,10 @@ export default class ChatModel {
} }
// utility // utility
static splitChannel(channelString: string): string[] {
return channelString.split("/");
}
static generateChatInfo = (primaryChannel: string): ChatInfo => { static generateChatInfo = (primaryChannel: string): ChatInfo => {
return { return {
dataVersion: DATA_VERSION, dataVersion: DATA_VERSION,
+5
View File
@@ -227,6 +227,7 @@ export enum StorageModelSubPath {
Chat = "chat", Chat = "chat",
ConnectionModel = "connection", ConnectionModel = "connection",
NotificationModel = "notifications",
SettingsModel = "settings", SettingsModel = "settings",
} }
@@ -253,6 +254,10 @@ export const filePaths = {
files: (id: string) => [...filePaths.chat.chatBase(id), "files"], files: (id: string) => [...filePaths.chat.chatBase(id), "files"],
}, },
notificationModel: {
base: [],
},
settingsModel: { settingsModel: {
username: ["user-name"], username: ["user-name"],
firstDayOfWeek: ["first-day-of-week"], firstDayOfWeek: ["first-day-of-week"],
+35 -4
View File
@@ -125,15 +125,46 @@ body[showing-chat] #chat-page-wrapper {
} }
#chat-page > div > #ribbon > span { #chat-page > div > #ribbon > span {
height: 100%;
flex: 1;
overflow-y: auto; overflow-y: auto;
flex-direction: column; flex-direction: column;
justify-content: end; justify-content: end;
} }
#chat-page > div > #ribbon > span.marquee {
height: 100%;
overflow: hidden;
position: relative;
}
#chat-page > div > #ribbon > span.marquee > span {
transform: rotate(270deg);
transform-origin: left;
position: absolute;
bottom: -10px;
left: calc(var(--control-height) / 2);
height: var(--control-height);
min-width: 100vh;
overflow: visible;
text-overflow: visible;
white-space: nowrap;
display: flex;
align-items: center;
gap: var(--gap-between-tiles);
}
#chat-page > div > #ribbon > span.marquee > span > * {
animation: slide-up var(--animation-duration);
}
#chat-page > div > #ribbon > .navigation-buttons {
min-height: calc(var(--control-height) * 4);
flex: 0;
}
#chat-page > div > #ribbon button, #chat-page > div > #ribbon button,
#chat-page > div > #main > div > .pane-wrapper > .pane > .toolbar button { #chat-page > div > #main > div > .pane-wrapper > .pane > .toolbar button {
padding: 0px; padding: 0px;
+18 -1
View File
@@ -34,6 +34,21 @@ export function ChatPage(chatViewModel: ChatViewModel) {
}, },
); );
const marqueeContent = React.createProxyState(
[chatViewModel.notificationViewModel.marquee],
() => {
const value = chatViewModel.notificationViewModel.marquee.value;
if (value == undefined) return <span></span>;
return (
<span>
<b>{value.sender}</b>
<span class="secondary">{value.chat}: </span>
<span>{value.body}</span>
</span>
);
},
);
return ( return (
<article <article
id="chat-page" id="chat-page"
@@ -63,7 +78,9 @@ export function ChatPage(chatViewModel: ChatViewModel) {
<span class="icon">signal_disconnected</span> <span class="icon">signal_disconnected</span>
</button> </button>
<span> <span class="marquee" children:set={marqueeContent}></span>
<span class="navigation-buttons">
{ChatViewToggleButton( {ChatViewToggleButton(
translations.chatPage.pages.calendar, translations.chatPage.pages.calendar,
"calendar_month", "calendar_month",
+5
View File
@@ -8,11 +8,14 @@ import { IndexManager } from "../../Model/Utility/utility";
import SettingsViewModel from "../Global/settingsViewModel"; import SettingsViewModel from "../Global/settingsViewModel";
import StorageModel from "../../Model/Global/storageModel"; import StorageModel from "../../Model/Global/storageModel";
import ConnectionViewModel from "../Global/connectionViewModel"; import ConnectionViewModel from "../Global/connectionViewModel";
import NotificationViewModel from "../Global/notificationViewModel";
import NotificationModel from "../../Model/Global/notificationModel";
export default class ChatListViewModel { export default class ChatListViewModel {
storageModel: StorageModel; storageModel: StorageModel;
chatListModel: ChatListModel; chatListModel: ChatListModel;
settingsViewModel: SettingsViewModel; settingsViewModel: SettingsViewModel;
notificationViewModel: NotificationViewModel;
connectionViewModel: ConnectionViewModel; connectionViewModel: ConnectionViewModel;
// data // data
@@ -63,6 +66,7 @@ export default class ChatListViewModel {
this.storageModel, this.storageModel,
chatModel, chatModel,
this.settingsViewModel, this.settingsViewModel,
this.notificationViewModel,
this.connectionViewModel, this.connectionViewModel,
this, this,
); );
@@ -105,6 +109,7 @@ export default class ChatListViewModel {
this.storageModel = storageModel; this.storageModel = storageModel;
this.chatListModel = chatListModel; this.chatListModel = chatListModel;
this.settingsViewModel = settingsViewModel; this.settingsViewModel = settingsViewModel;
this.notificationViewModel = new NotificationViewModel(this);
this.connectionViewModel = connectionViewModel; this.connectionViewModel = connectionViewModel;
this.loadChats(); this.loadChats();
+9 -3
View File
@@ -19,11 +19,13 @@ import SettingsViewModel from "../Global/settingsViewModel";
import TaskPageViewModel from "../Pages/taskPageViewModel"; import TaskPageViewModel from "../Pages/taskPageViewModel";
import { translations } from "../../View/translations"; import { translations } from "../../View/translations";
import ConnectionViewModel from "../Global/connectionViewModel"; import ConnectionViewModel from "../Global/connectionViewModel";
import NotificationViewModel from "../Global/notificationViewModel";
export default class ChatViewModel { export default class ChatViewModel {
chatModel: ChatModel; chatModel: ChatModel;
storageModel: StorageModel; storageModel: StorageModel;
settingsViewModel: SettingsViewModel; settingsViewModel: SettingsViewModel;
notificationViewModel: NotificationViewModel;
connectionViewModel: ConnectionViewModel; connectionViewModel: ConnectionViewModel;
chatListViewModel: ChatListViewModel; chatListViewModel: ChatListViewModel;
@@ -53,7 +55,7 @@ export default class ChatViewModel {
this.chatListViewModel.closeChat(); this.chatListViewModel.closeChat();
}; };
closeSubPages = (): void => { }; closeSubPages = (): void => {};
setColor = (color: Color): void => { setColor = (color: Color): void => {
this.setDisplayedColor(color); this.setDisplayedColor(color);
@@ -104,12 +106,12 @@ export default class ChatViewModel {
this.chatModel.setReadStatus(false); this.chatModel.setReadStatus(false);
} }
this.hasUnreadMessages.value = this.chatModel.info.hasUnreadMessages; this.hasUnreadMessages.value = this.chatModel.info.hasUnreadMessages;
} };
setReadStatus = (hasUnreadMessages: boolean): void => { setReadStatus = (hasUnreadMessages: boolean): void => {
this.chatModel.setReadStatus(hasUnreadMessages); this.chatModel.setReadStatus(hasUnreadMessages);
this.hasUnreadMessages.value = hasUnreadMessages; this.hasUnreadMessages.value = hasUnreadMessages;
} };
// init // init
constructor( constructor(
@@ -117,6 +119,7 @@ export default class ChatViewModel {
storageModel: StorageModel, storageModel: StorageModel,
chatModel: ChatModel, chatModel: ChatModel,
settingsViewModel: SettingsViewModel, settingsViewModel: SettingsViewModel,
notificationViewModel: NotificationViewModel,
connectionViewModel: ConnectionViewModel, connectionViewModel: ConnectionViewModel,
chatListViewModel: ChatListViewModel, chatListViewModel: ChatListViewModel,
) { ) {
@@ -125,6 +128,7 @@ export default class ChatViewModel {
this.chatModel = chatModel; this.chatModel = chatModel;
this.settingsViewModel = settingsViewModel; this.settingsViewModel = settingsViewModel;
this.connectionViewModel = connectionViewModel; this.connectionViewModel = connectionViewModel;
this.notificationViewModel = notificationViewModel;
this.chatListViewModel = chatListViewModel; this.chatListViewModel = chatListViewModel;
// page viewModels // page viewModels
@@ -155,6 +159,8 @@ export default class ChatViewModel {
(chatMessage: ChatMessage) => { (chatMessage: ChatMessage) => {
this.messagePageViewModel.showChatMessage(chatMessage); this.messagePageViewModel.showChatMessage(chatMessage);
this.updateReadStatus(); this.updateReadStatus();
this.notificationViewModel.showNotification(chatMessage);
}, },
); );
@@ -0,0 +1,74 @@
import * as React from "bloatless-react";
import ChatModel, { ChatMessage } from "../../Model/Chat/chatModel";
import ChatListViewModel from "../Chat/chatListViewModel";
export default class NotificationViewModel {
chatListViewModel: ChatListViewModel;
// data
seenMessageIds = new Set<string>();
messagesInMarquee: Notification[] = [];
marquee = new React.State<Notification | undefined>(undefined);
currentIndex = 0;
interval: number | undefined = undefined;
// main
showNotification = (message: ChatMessage): void => {
const notification = NotificationViewModel.createNotification(message);
if (this.seenMessageIds.has(message.id)) return;
const currentChat = this.chatListViewModel.selectedChat.value.chatModel.info.primaryChannel;
if (notification.chat == currentChat) return;
this.messagesInMarquee.push(notification);
this.startLoop();
};
// loop
loop = () => {
if (this.messagesInMarquee.length == 0) {
this.marquee.value = undefined;
return this.stopLoop();
}
const notification = this.messagesInMarquee.shift();
this.seenMessageIds.delete(notification.messageId);
this.marquee.value = notification;
};
startLoop = () => {
if (this.interval != undefined) return;
this.loop();
this.interval = setInterval(() => {
this.loop();
}, 5000);
};
stopLoop = () => {
clearInterval(this.interval);
this.interval = undefined;
};
// init
constructor(chatListViewModel: ChatListViewModel) {
this.chatListViewModel = chatListViewModel;
}
// util
static createNotification(message: ChatMessage): Notification {
return {
messageId: message.id,
chat: ChatModel.splitChannel(message.channel)[0],
sender: message.sender,
body: message.body,
};
}
}
export interface Notification {
messageId: string;
chat: string;
sender: string;
body: string;
}