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 + );