diff --git a/dist/index.css b/dist/index.css
index 15f45149..03fbc791 100644
--- a/dist/index.css
+++ b/dist/index.css
@@ -340,12 +340,37 @@ body[showing-chat] #chat-page-wrapper {
justify-content: space-between;
}
#chat-page > div > #ribbon > span {
- height: 100%;
- flex: 1;
overflow-y: auto;
flex-direction: column;
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 > #main > div > .pane-wrapper > .pane > .toolbar button {
padding: 0px;
diff --git a/dist/index.js b/dist/index.js
index 442a634c..65e8078e 100644
--- a/dist/index.js
+++ b/dist/index.js
@@ -651,6 +651,9 @@
],
files: (id) => [...filePaths.chat.chatBase(id), "files"]
},
+ notificationModel: {
+ base: []
+ },
settingsModel: {
username: ["user-name"],
firstDayOfWeek: ["first-day-of-week"]
@@ -1373,8 +1376,11 @@
);
return sorted;
}
+ // utility
+ static splitChannel(channelString) {
+ return channelString.split("/");
+ }
static {
- // utility
this.generateChatInfo = (primaryChannel) => {
return {
dataVersion: DATA_VERSION,
@@ -3024,7 +3030,7 @@
// src/ViewModel/Chat/chatViewModel.ts
var ChatViewModel = class {
// init
- constructor(coreViewModel, storageModel2, chatModel, settingsViewModel2, connectionViewModel2, chatListViewModel2) {
+ constructor(coreViewModel, storageModel2, chatModel, settingsViewModel2, notificationViewModel, connectionViewModel2, chatListViewModel2) {
this.coreViewModel = coreViewModel;
// state
this.displayedColor = new State("standard" /* Standard */);
@@ -3094,6 +3100,7 @@
this.chatModel = chatModel;
this.settingsViewModel = settingsViewModel2;
this.connectionViewModel = connectionViewModel2;
+ this.notificationViewModel = notificationViewModel;
this.chatListViewModel = chatListViewModel2;
this.calendarViewModel = new CalendarPageViewModel(
coreViewModel,
@@ -3120,6 +3127,7 @@
(chatMessage) => {
this.messagePageViewModel.showChatMessage(chatMessage);
this.updateReadStatus();
+ this.notificationViewModel.showNotification(chatMessage);
}
);
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
var ChatListViewModel = class {
// init
@@ -3171,6 +3232,7 @@
this.storageModel,
chatModel,
this.settingsViewModel,
+ this.notificationViewModel,
this.connectionViewModel,
this
);
@@ -3200,6 +3262,7 @@
this.storageModel = storageModel2;
this.chatListModel = chatListModel2;
this.settingsViewModel = settingsViewModel2;
+ this.notificationViewModel = new NotificationViewModel(this);
this.connectionViewModel = connectionViewModel2;
this.loadChats();
}
@@ -3680,7 +3743,7 @@
}
// src/View/Components/chatMessage.tsx
- function ChatMessage2(chatMessageViewModel) {
+ function ChatMessage3(chatMessageViewModel) {
const statusIcon = createProxyState(
[chatMessageViewModel.status],
() => {
@@ -3726,7 +3789,7 @@
);
}
var ChatMessageViewModelToView = (chatMessageViewModel) => {
- return ChatMessage2(chatMessageViewModel);
+ return ChatMessage3(chatMessageViewModel);
};
// 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(
"article",
{
@@ -4579,7 +4650,7 @@
"toggle:hidden": chatViewModel.connectionViewModel.isConnected
},
/* @__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,
"calendar_month",
"calendar" /* Calendar */,
diff --git a/src/Model/Chat/chatModel.ts b/src/Model/Chat/chatModel.ts
index ddcc0301..bbe5fc51 100644
--- a/src/Model/Chat/chatModel.ts
+++ b/src/Model/Chat/chatModel.ts
@@ -84,7 +84,7 @@ export default class ChatModel {
chatMessage.status = ChatMessageStatus.Received;
this.addMessage(chatMessage);
-
+
this.setReadStatus(true);
};
@@ -271,6 +271,10 @@ export default class ChatModel {
}
// utility
+ static splitChannel(channelString: string): string[] {
+ return channelString.split("/");
+ }
+
static generateChatInfo = (primaryChannel: string): ChatInfo => {
return {
dataVersion: DATA_VERSION,
diff --git a/src/Model/Global/storageModel.ts b/src/Model/Global/storageModel.ts
index ea1d3571..490bcf48 100644
--- a/src/Model/Global/storageModel.ts
+++ b/src/Model/Global/storageModel.ts
@@ -227,6 +227,7 @@ export enum StorageModelSubPath {
Chat = "chat",
ConnectionModel = "connection",
+ NotificationModel = "notifications",
SettingsModel = "settings",
}
@@ -253,6 +254,10 @@ export const filePaths = {
files: (id: string) => [...filePaths.chat.chatBase(id), "files"],
},
+ notificationModel: {
+ base: [],
+ },
+
settingsModel: {
username: ["user-name"],
firstDayOfWeek: ["first-day-of-week"],
diff --git a/src/View/chatPage.css b/src/View/chatPage.css
index a29982bb..db56dfe5 100644
--- a/src/View/chatPage.css
+++ b/src/View/chatPage.css
@@ -125,15 +125,46 @@ body[showing-chat] #chat-page-wrapper {
}
#chat-page > div > #ribbon > span {
- height: 100%;
-
- flex: 1;
overflow-y: auto;
-
flex-direction: column;
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 > #main > div > .pane-wrapper > .pane > .toolbar button {
padding: 0px;
diff --git a/src/View/chatPage.tsx b/src/View/chatPage.tsx
index 8dccac58..66bc16f5 100644
--- a/src/View/chatPage.tsx
+++ b/src/View/chatPage.tsx
@@ -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 ;
+ return (
+
+ {value.sender}
+ {value.chat}:
+ {value.body}
+
+ );
+ },
+ );
+
return (
signal_disconnected
-
+
+
+
{ChatViewToggleButton(
translations.chatPage.pages.calendar,
"calendar_month",
diff --git a/src/ViewModel/Chat/chatListViewModel.ts b/src/ViewModel/Chat/chatListViewModel.ts
index 7d960800..93e324ff 100644
--- a/src/ViewModel/Chat/chatListViewModel.ts
+++ b/src/ViewModel/Chat/chatListViewModel.ts
@@ -8,11 +8,14 @@ import { IndexManager } from "../../Model/Utility/utility";
import SettingsViewModel from "../Global/settingsViewModel";
import StorageModel from "../../Model/Global/storageModel";
import ConnectionViewModel from "../Global/connectionViewModel";
+import NotificationViewModel from "../Global/notificationViewModel";
+import NotificationModel from "../../Model/Global/notificationModel";
export default class ChatListViewModel {
storageModel: StorageModel;
chatListModel: ChatListModel;
settingsViewModel: SettingsViewModel;
+ notificationViewModel: NotificationViewModel;
connectionViewModel: ConnectionViewModel;
// data
@@ -63,6 +66,7 @@ export default class ChatListViewModel {
this.storageModel,
chatModel,
this.settingsViewModel,
+ this.notificationViewModel,
this.connectionViewModel,
this,
);
@@ -105,6 +109,7 @@ export default class ChatListViewModel {
this.storageModel = storageModel;
this.chatListModel = chatListModel;
this.settingsViewModel = settingsViewModel;
+ this.notificationViewModel = new NotificationViewModel(this);
this.connectionViewModel = connectionViewModel;
this.loadChats();
diff --git a/src/ViewModel/Chat/chatViewModel.ts b/src/ViewModel/Chat/chatViewModel.ts
index 059d46ce..bf4b1a66 100644
--- a/src/ViewModel/Chat/chatViewModel.ts
+++ b/src/ViewModel/Chat/chatViewModel.ts
@@ -19,11 +19,13 @@ import SettingsViewModel from "../Global/settingsViewModel";
import TaskPageViewModel from "../Pages/taskPageViewModel";
import { translations } from "../../View/translations";
import ConnectionViewModel from "../Global/connectionViewModel";
+import NotificationViewModel from "../Global/notificationViewModel";
export default class ChatViewModel {
chatModel: ChatModel;
storageModel: StorageModel;
settingsViewModel: SettingsViewModel;
+ notificationViewModel: NotificationViewModel;
connectionViewModel: ConnectionViewModel;
chatListViewModel: ChatListViewModel;
@@ -53,7 +55,7 @@ export default class ChatViewModel {
this.chatListViewModel.closeChat();
};
- closeSubPages = (): void => { };
+ closeSubPages = (): void => {};
setColor = (color: Color): void => {
this.setDisplayedColor(color);
@@ -104,12 +106,12 @@ export default class ChatViewModel {
this.chatModel.setReadStatus(false);
}
this.hasUnreadMessages.value = this.chatModel.info.hasUnreadMessages;
- }
+ };
setReadStatus = (hasUnreadMessages: boolean): void => {
this.chatModel.setReadStatus(hasUnreadMessages);
this.hasUnreadMessages.value = hasUnreadMessages;
- }
+ };
// init
constructor(
@@ -117,6 +119,7 @@ export default class ChatViewModel {
storageModel: StorageModel,
chatModel: ChatModel,
settingsViewModel: SettingsViewModel,
+ notificationViewModel: NotificationViewModel,
connectionViewModel: ConnectionViewModel,
chatListViewModel: ChatListViewModel,
) {
@@ -125,6 +128,7 @@ export default class ChatViewModel {
this.chatModel = chatModel;
this.settingsViewModel = settingsViewModel;
this.connectionViewModel = connectionViewModel;
+ this.notificationViewModel = notificationViewModel;
this.chatListViewModel = chatListViewModel;
// page viewModels
@@ -155,6 +159,8 @@ export default class ChatViewModel {
(chatMessage: ChatMessage) => {
this.messagePageViewModel.showChatMessage(chatMessage);
this.updateReadStatus();
+
+ this.notificationViewModel.showNotification(chatMessage);
},
);
diff --git a/src/ViewModel/Global/notificationViewModel.ts b/src/ViewModel/Global/notificationViewModel.ts
new file mode 100644
index 00000000..e488bbe6
--- /dev/null
+++ b/src/ViewModel/Global/notificationViewModel.ts
@@ -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();
+ messagesInMarquee: Notification[] = [];
+ marquee = new React.State(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;
+}
diff --git a/src/ViewModel/Pages/calendarPageViewModel.ts b/src/ViewModel/Pages/calendarPageViewModel.ts
index d8fbfa9d..427118e3 100644
--- a/src/ViewModel/Pages/calendarPageViewModel.ts
+++ b/src/ViewModel/Pages/calendarPageViewModel.ts
@@ -212,7 +212,7 @@ export default class CalendarPageViewModel extends TaskContainingPageViewModel {
this.showTask(taskFileContent);
},
);
-
+
// initiate
this.showToday();
}
diff --git a/src/index.tsx b/src/index.tsx
index 9841a39d..a6720736 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -85,4 +85,4 @@ document
ConnectionModal(connectionViewModel),
DataTransferModalWrapper(connectionViewModel, fileTransferViewModel),
StorageModal(storageViewModel),
- );
\ No newline at end of file
+ );