This commit is contained in:
marlon-erler
2026-09-26 23:32:38 +02:00
parent ce9f6d89f0
commit 9622c196cd
40 changed files with 2866 additions and 2706 deletions
+22 -13
View File
@@ -97,8 +97,11 @@ export default class ChatModel {
}; };
readonly getPreviousFilterPath = (): string[] => { readonly getPreviousFilterPath = (): string[] => {
return [StorageModelSubPaths.Chat, ...filePaths.chat.previousFilter(this.id)]; return [
} StorageModelSubPaths.Chat,
...filePaths.chat.previousFilter(this.id),
];
};
// handlers // handlers
readonly handleMessage = (body: string): void => { readonly handleMessage = (body: string): void => {
@@ -195,7 +198,7 @@ export default class ChatModel {
body: string, body: string,
inlineReplyId?: string, inlineReplyId?: string,
fileContent?: FileContent<string>, fileContent?: FileContent<string>,
): Promise<string|false> => { ): Promise<string | false> => {
const nameAndChannel = this.getNameAndChannel(); const nameAndChannel = this.getNameAndChannel();
if (nameAndChannel == false) return false; if (nameAndChannel == false) return false;
const [senderName, combinedChannel] = nameAndChannel; const [senderName, combinedChannel] = nameAndChannel;
@@ -264,15 +267,15 @@ export default class ChatModel {
readonly syncInfo = (): void => { readonly syncInfo = (): void => {
this.sendMessage("", undefined, this.info); this.sendMessage("", undefined, this.info);
} };
readonly storeFilter = (filter: string): void => { readonly storeFilter = (filter: string): void => {
this.storageModel.write(this.getPreviousFilterPath(), filter); this.storageModel.write(this.getPreviousFilterPath(), filter);
} };
readonly getFilter = (): string=> { readonly getFilter = (): string => {
return this.storageModel.read(this.getPreviousFilterPath()) || ""; return this.storageModel.read(this.getPreviousFilterPath()) || "";
} };
readonly delete = () => { readonly delete = () => {
// untrack // untrack
@@ -285,11 +288,13 @@ export default class ChatModel {
// load // load
readonly loadInfo = (): void => { readonly loadInfo = (): void => {
let info: ChatInfoFileContent | null = this.storageModel.readStringifiable( let info: ChatInfoFileContent | null =
this.storageModel.readStringifiable(
this.getInfoPath(), this.getInfoPath(),
ChatInfoReference, ChatInfoReference,
); );
this.info = (info || ChatModel.generateChatInfo("0", "0", Colors.Standard)); this.info =
info || ChatModel.generateChatInfo("0", "0", Colors.Standard);
this.storeInfo(); this.storeInfo();
}; };
@@ -297,7 +302,7 @@ export default class ChatModel {
this.info = info; this.info = info;
this.changeHandlerManager.trigger(null); this.changeHandlerManager.trigger(null);
this.storeInfo(); this.storeInfo();
} };
get messages(): ChatMessage[] { get messages(): ChatMessage[] {
const messageIds: string[] = this.storageModel.list( const messageIds: string[] = this.storageModel.list(
@@ -373,7 +378,11 @@ export default class ChatModel {
return channelString.split("/"); return channelString.split("/");
} }
static generateChatInfo = (name: string, id: string, color: Colors): ChatInfoFileContent => { static generateChatInfo = (
name: string,
id: string,
color: Colors,
): ChatInfoFileContent => {
const file = FileModel.createFileContent(id, "chat-info"); const file = FileModel.createFileContent(id, "chat-info");
return { return {
@@ -498,7 +507,7 @@ export interface ChatMessage extends ValidObject {
} }
export interface ChatMessageReaction export interface ChatMessageReaction
extends ValidObject, FileContent<"reaction"> { extends ValidObject, FileContent<"reaction"> {
readonly messageId: string; readonly messageId: string;
readonly sender: string; readonly sender: string;
readonly content: ReactionSymbols | string; readonly content: ReactionSymbols | string;
@@ -552,4 +561,4 @@ export const ChatMessageReactionReference: ChatMessageReaction = {
content: "", content: "",
isDeleting: false, isDeleting: false,
} };
+3 -7
View File
@@ -2,10 +2,10 @@
import FileModel, { FileModelSubPath } from "./fileModel"; import FileModel, { FileModelSubPath } from "./fileModel";
import { TaskFileContent } from "./boardsAndTasksModel"; import { TaskFileContent } from "./boardsAndTasksModel";
import { isoToMonthString, padZero } from "../Utility/utility";
import StorageModel from "../Global/storageModel"; import StorageModel from "../Global/storageModel";
import SettingsModel from "../Global/settingsModel"; import SettingsModel from "../Global/settingsModel";
import CoreViewModel from "../../ViewModel/Global/coreViewModel"; import CoreViewModel from "../../ViewModel/Global/coreViewModel";
import {isoToMonthString, padZero} from "../Utility/utility";
export default class CalendarModel { export default class CalendarModel {
readonly storageModel: StorageModel; readonly storageModel: StorageModel;
@@ -35,9 +35,7 @@ export default class CalendarModel {
readonly storeTaskReference = (taskFileContent: TaskFileContent): void => { readonly storeTaskReference = (taskFileContent: TaskFileContent): void => {
if (taskFileContent.date == undefined) return; if (taskFileContent.date == undefined) return;
const monthString: string = isoToMonthString( const monthString: string = isoToMonthString(taskFileContent.date);
taskFileContent.date,
);
const monthPath: string[] = this.getMonthPath(monthString); const monthPath: string[] = this.getMonthPath(monthString);
const referencePath: string[] = [...monthPath, taskFileContent.fileId]; const referencePath: string[] = [...monthPath, taskFileContent.fileId];
@@ -103,9 +101,7 @@ export default class CalendarModel {
}; };
for (let i = 0; i < daysInMonth; i++) { for (let i = 0; i < daysInMonth; i++) {
const paddedDate: string = padZero( const paddedDate: string = padZero((i + 1).toString(), 2);
(i + 1).toString(), 2
);
grid.days[paddedDate] = defaultValueCreator(); grid.days[paddedDate] = defaultValueCreator();
} }
+16 -4
View File
@@ -8,13 +8,21 @@ import {
parseValidObject, parseValidObject,
stringify, stringify,
} from "../Utility/utility"; } from "../Utility/utility";
import { checkIsValidObject, checkMatchesObjectStructure, DATA_VERSION, ValidObject } from "../Utility/typeSafety"; import {
checkIsValidObject,
checkMatchesObjectStructure,
DATA_VERSION,
ValidObject,
} from "../Utility/typeSafety";
import StorageModel, { import StorageModel, {
StorageModelSubPaths, StorageModelSubPaths,
filePaths, filePaths,
} from "../Global/storageModel"; } from "../Global/storageModel";
import SettingsModel from "../Global/settingsModel"; import SettingsModel from "../Global/settingsModel";
import ChatModel, {ChatInfoFileContent, ChatInfoReference} from "../Chat/chatModel"; import ChatModel, {
ChatInfoFileContent,
ChatInfoReference,
} from "../Chat/chatModel";
export default class FileModel { export default class FileModel {
readonly storageModel: StorageModel; readonly storageModel: StorageModel;
@@ -70,7 +78,11 @@ export default class FileModel {
console.log(fileContent.type); console.log(fileContent.type);
if (fileContent.type == "chat-info") { if (fileContent.type == "chat-info") {
if (checkMatchesObjectStructure(fileContent, ChatInfoReference) == false) return; if (
checkMatchesObjectStructure(fileContent, ChatInfoReference) ==
false
)
return;
this.chatModel.handleInfo(fileContent as ChatInfoFileContent); this.chatModel.handleInfo(fileContent as ChatInfoFileContent);
} }
@@ -221,7 +233,7 @@ export interface FileContent<T extends string>
readonly creationDate: string; readonly creationDate: string;
readonly type: T; readonly type: T;
} }
// references // references
export const FileContentReference: FileContent<string> = { export const FileContentReference: FileContent<string> = {
+14 -7
View File
@@ -1,5 +1,9 @@
import {
import { bytesToMB, localeCompare, parseValidObject, stringify } from "../Utility/utility"; bytesToMB,
localeCompare,
parseValidObject,
stringify,
} from "../Utility/utility";
import { DATA_VERSION, ValidObject } from "../Utility/typeSafety"; import { DATA_VERSION, ValidObject } from "../Utility/typeSafety";
export const PATH_COMPONENT_SEPARATOR = "\\"; export const PATH_COMPONENT_SEPARATOR = "\\";
@@ -155,7 +159,7 @@ export default class StorageModel {
const KEY = "storage-capacity-check"; const KEY = "storage-capacity-check";
let chunk = "0"; let chunk = "0";
for (let i = 0; i < 1024*256; i++) { for (let i = 0; i < 1024 * 256; i++) {
chunk += "0"; chunk += "0";
} }
@@ -171,7 +175,7 @@ export default class StorageModel {
return capacity; return capacity;
} }
} }
} };
readonly calculateUsage = (): number => { readonly calculateUsage = (): number => {
let data = JSON.stringify(localStorage); let data = JSON.stringify(localStorage);
@@ -179,9 +183,9 @@ export default class StorageModel {
const encoded = encoder.encode(data); const encoded = encoder.encode(data);
const bytes = encoded.byteLength; const bytes = encoded.byteLength;
const mb = bytesToMB(bytes); const mb = bytesToMB(bytes);
const rounded = Math.round(mb * 100)/100; const rounded = Math.round(mb * 100) / 100;
return rounded; return rounded;
} };
// tree // tree
readonly initializeTree = (): void => { readonly initializeTree = (): void => {
@@ -285,7 +289,10 @@ export const filePaths = {
...filePaths.chat.chatBase(id), ...filePaths.chat.chatBase(id),
"reactions", "reactions",
], ],
previousFilter: (id: string) => [...filePaths.chat.chatBase(id), "previous-filter"], previousFilter: (id: string) => [
...filePaths.chat.chatBase(id),
"previous-filter",
],
lastUsedPage: (id: string) => [ lastUsedPage: (id: string) => [
...filePaths.chat.chatBase(id), ...filePaths.chat.chatBase(id),
"last-used-page", "last-used-page",
+52 -33
View File
@@ -1,7 +1,7 @@
import { checkIsValidObject, checkMatchesObjectStructure } from "./typeSafety"; import { checkIsValidObject, checkMatchesObjectStructure } from "./typeSafety";
import * as React from "../../react";
import {CalendarPage} from "../../View/ChatPages/calendarPage";
import CalendarModel from "../Files/calendarModel"; import CalendarModel from "../Files/calendarModel";
import * as React from "../../react";
import { CalendarPage } from "../../View/ChatPages/calendarPage";
export interface Stringifiable { export interface Stringifiable {
toString(): string; toString(): string;
@@ -20,36 +20,47 @@ export function createTimestamp(): string {
return new Date().toISOString(); return new Date().toISOString();
} }
export function isoToDateString (dateISOString: string): string { export function isoToDateString(dateISOString: string): string {
const [year, month, date] = dateISOString.split("-"); const [year, month, date] = dateISOString.split("-");
const paddedDate = padZero(date ?? "", 2); const paddedDate = padZero(date ?? "", 2);
return paddedDate; return paddedDate;
};
export function isoToMonthString (dateISOString: string): string {
const [year, month, _] = dateISOString.split("-");
return formatMonthStringFromParts(year, month);
};
export function formatISO(date: Date): string {
return formatISOFromParts(date.getFullYear(), date.getMonth()+1, date.getDate())
} }
export function formatISOFromParts(year: number|string, month: number|string, date: number|string): string { export function isoToMonthString(dateISOString: string): string {
const [year, month, _] = dateISOString.split("-");
return formatMonthStringFromParts(year, month);
}
export function formatISO(date: Date): string {
return formatISOFromParts(
date.getFullYear(),
date.getMonth() + 1,
date.getDate(),
);
}
export function formatISOFromParts(
year: number | string,
month: number | string,
date: number | string,
): string {
return [year, month, date] return [year, month, date]
.map((x, i) => i == 0 ? x : padZero(x.toString(), 2)) .map((x, i) => (i == 0 ? x : padZero(x.toString(), 2)))
.join("-"); .join("-");
} }
export function formatMonthStringFromParts (year: string = "", month: string = ""): string { export function formatMonthStringFromParts(
year: string = "",
month: string = "",
): string {
const paddedYear: string = padZero(year, 4); const paddedYear: string = padZero(year, 4);
const paddedMonth: string = padZero(month, 2); const paddedMonth: string = padZero(month, 2);
return `${paddedYear}-${paddedMonth}`; return `${paddedYear}-${paddedMonth}`;
}; }
export function formatTime(date: Date): string { export function formatTime(date: Date): string {
return [date.getHours(), date.getMinutes()] return [date.getHours(), date.getMinutes()]
.map(x => padZero(x.toString(), 2)) .map((x) => padZero(x.toString(), 2))
.join(":"); .join(":");
} }
@@ -133,7 +144,7 @@ export function checkDoesObjectMatchSearch<T>(
} }
export default function implementFilter<T>( export default function implementFilter<T>(
allItems: React.ListState<T>|React.MapState<T>, allItems: React.ListState<T> | React.MapState<T>,
matches: React.ListState<T>, matches: React.ListState<T>,
query: React.State<string>, query: React.State<string>,
itemToString: (item: T) => string, itemToString: (item: T) => string,
@@ -141,7 +152,12 @@ export default function implementFilter<T>(
function update() { function update() {
matches.clear(); matches.clear();
allItems.value.forEach((item: T) => { allItems.value.forEach((item: T) => {
if (!itemToString(item).toLowerCase().includes(query.value.toLowerCase())) return; if (
!itemToString(item)
.toLowerCase()
.includes(query.value.toLowerCase())
)
return;
matches.add(item); matches.add(item);
allItems.handleRemoval(item, () => { allItems.handleRemoval(item, () => {
matches.remove(item); matches.remove(item);
@@ -213,7 +229,7 @@ export function getLocalStorageItemAndClear(key: string): string | null {
} }
export function bytesToMB(bytes: number): number { export function bytesToMB(bytes: number): number {
return bytes / (1024*1024); return bytes / (1024 * 1024);
} }
// string & parsing // string & parsing
@@ -271,7 +287,10 @@ export interface PinchToZoomData {
x: number; x: number;
y: number; y: number;
} }
export function implementPinchZoom(canvas: HTMLElement, data: React.State<PinchToZoomData>) { export function implementPinchZoom(
canvas: HTMLElement,
data: React.State<PinchToZoomData>,
) {
let pinching = false; let pinching = false;
let dragging = false; let dragging = false;
let lastElement: HTMLElement | undefined = undefined; let lastElement: HTMLElement | undefined = undefined;
@@ -314,19 +333,21 @@ export function implementPinchZoom(canvas: HTMLElement, data: React.State<PinchT
if (zoom < MIN) return apply(MIN, _offset); if (zoom < MIN) return apply(MIN, _offset);
if (zoom > MAX) return apply(5, _offset); if (zoom > MAX) return apply(5, _offset);
let {x, y} = data.value; let { x, y } = data.value;
if (_offset) [x, y] = _offset; if (_offset) [x, y] = _offset;
data.value = { data.value = {
zoom, x, y zoom,
} x,
y,
};
} }
data.subscribe((data) => { data.subscribe((data) => {
const el = element(); const el = element();
if (!el) return; if (!el) return;
el.style.transform = `scale(${data.zoom.toString()}) translate(${data.x}px, ${data.y}px)`; el.style.transform = `scale(${data.zoom.toString()}) translate(${data.x}px, ${data.y}px)`;
}) });
canvas.addEventListener("wheel", (event: WheelEvent) => { canvas.addEventListener("wheel", (event: WheelEvent) => {
event.preventDefault(); event.preventDefault();
@@ -342,7 +363,7 @@ export function implementPinchZoom(canvas: HTMLElement, data: React.State<PinchT
}); });
canvas.addEventListener("mousedown", (event: MouseEvent) => { canvas.addEventListener("mousedown", (event: MouseEvent) => {
const target = event.target as HTMLElement|undefined; const target = event.target as HTMLElement | undefined;
console.log(target); console.log(target);
if (!target || !target.classList.contains("allow-drag-move")) return; if (!target || !target.classList.contains("allow-drag-move")) return;
event.preventDefault(); event.preventDefault();
@@ -352,20 +373,18 @@ export function implementPinchZoom(canvas: HTMLElement, data: React.State<PinchT
initialTouchX = event.clientX; initialTouchX = event.clientX;
initialTouchY = event.clientY; initialTouchY = event.clientY;
dragging = true; dragging = true;
}) });
canvas.addEventListener("mouseup", () => { canvas.addEventListener("mouseup", () => {
dragging = false; dragging = false;
}) });
canvas.addEventListener("mousemove", (event: MouseEvent) => { canvas.addEventListener("mousemove", (event: MouseEvent) => {
if (!dragging) return; if (!dragging) return;
event.preventDefault(); event.preventDefault();
apply(data.value.zoom, [ apply(data.value.zoom, [
initialX + initialX + (event.clientX - initialTouchX) / initialZoom,
(event.clientX - initialTouchX) / initialZoom, initialY + (event.clientY - initialTouchY) / initialZoom,
initialY +
(event.clientY - initialTouchY) / initialZoom,
]); ]);
}) });
canvas.addEventListener("touchstart", (event: TouchEvent) => { canvas.addEventListener("touchstart", (event: TouchEvent) => {
(document.activeElement as HTMLElement)?.blur(); (document.activeElement as HTMLElement)?.blur();
+4 -1
View File
@@ -46,7 +46,10 @@ export function BoardPage(
); );
const wrapper = ( const wrapper = (
<div class="content main-content allow-drag-move" children:set={pages}></div> <div
class="content main-content allow-drag-move"
children:set={pages}
></div>
); );
Utility.implementPinchZoom(wrapper, boardViewModel.pinchToZoomData); Utility.implementPinchZoom(wrapper, boardViewModel.pinchToZoomData);
+2 -1
View File
@@ -285,7 +285,8 @@
padding: var(--gap-between-tiles); padding: var(--gap-between-tiles);
} }
#message-page[reply-view] :is(.reply-link, .inline-reply, #composer .reply-preview-wrapper) { #message-page[reply-view]
:is(.reply-link, .inline-reply, #composer .reply-preview-wrapper) {
display: none; display: none;
} }
+47 -26
View File
@@ -16,11 +16,11 @@ export function MessagePage(
const ChatMessageViewModelToView: React.StateItemConverter< const ChatMessageViewModelToView: React.StateItemConverter<
ChatMessageViewModel ChatMessageViewModel
> = (chatMessageViewModel: ChatMessageViewModel) => { > = (chatMessageViewModel: ChatMessageViewModel) => {
return ChatMessage(coreViewModel, chatMessageViewModel); return ChatMessage(coreViewModel, chatMessageViewModel);
}; };
const messageContainer = ( const messageContainer = (
<div <div
id="message-container" id="message-container"
children:append={[ children:append={[
@@ -28,10 +28,10 @@ const messageContainer = (
ChatMessageViewModelToView, ChatMessageViewModelToView,
]} ]}
></div> ></div>
); );
const persistenceId = messagePageViewModel.chatViewModel.chatModel.id; const persistenceId = messagePageViewModel.chatViewModel.chatModel.id;
const replyPreview = ViewController.inlineReplies.setState( const replyPreview = ViewController.inlineReplies.setState(
persistenceId, persistenceId,
() => () =>
React.createProxyState( React.createProxyState(
@@ -45,42 +45,62 @@ const replyPreview = ViewController.inlineReplies.setState(
); );
}, },
), ),
); );
function scrollDown(hard: boolean = false) { function scrollDown(hard: boolean = false) {
if (hard) { if (hard) {
messageContainer.setAttribute("scroll-hard", ""); messageContainer.setAttribute("scroll-hard", "");
} }
messageContainer.scrollTop = messageContainer.scrollHeight; messageContainer.scrollTop = messageContainer.scrollHeight;
messageContainer.removeAttribute("scroll-hard"); messageContainer.removeAttribute("scroll-hard");
} }
function scrollDownIfApplicable() { function scrollDownIfApplicable() {
const scrollFromBottom = const scrollFromBottom =
messageContainer.scrollHeight - messageContainer.scrollHeight -
(messageContainer.scrollTop + messageContainer.offsetHeight); (messageContainer.scrollTop + messageContainer.offsetHeight);
if (scrollFromBottom > 400) return; if (scrollFromBottom > 400) return;
scrollDown(); scrollDown();
} }
messagePageViewModel.filteredMessageViewModels.subscribeSilent( messagePageViewModel.filteredMessageViewModels.subscribeSilent(
scrollDownIfApplicable, scrollDownIfApplicable,
); );
React.bulkSubscribe([messagePageViewModel.reactionFilter, messagePageViewModel.searchViewModel.appliedQuery], scrollDown) React.bulkSubscribe(
messagePageViewModel.replyViewSelectedMessage.subscribeSilent(selectedMessage => { [
messagePageViewModel.reactionFilter,
messagePageViewModel.searchViewModel.appliedQuery,
],
scrollDown,
);
messagePageViewModel.replyViewSelectedMessage.subscribeSilent(
(selectedMessage) => {
if (selectedMessage != undefined) return; if (selectedMessage != undefined) return;
setTimeout(scrollDown, 100); setTimeout(scrollDown, 100);
}) },
setTimeout(() => scrollDown(true), 100); );
setTimeout(() => scrollDown(true), 100);
messagePageViewModel.focusSetter.subscribeSilent(() => { messagePageViewModel.focusSetter.subscribeSilent(() => {
ViewController.setFocusWithDelay(); ViewController.setFocusWithDelay();
}); });
const isInReplyView = React.createProxyState([messagePageViewModel.replyViewSelectedMessage], ()=>messagePageViewModel.replyViewSelectedMessage.value != undefined); const isInReplyView = React.createProxyState(
[messagePageViewModel.replyViewSelectedMessage],
() => messagePageViewModel.replyViewSelectedMessage.value != undefined,
);
const title = React.createProxyState([messagePageViewModel.isReplyViewActive], ()=>messagePageViewModel.isReplyViewActive.value ? coreViewModel.translations.chatPage.message.replyHeaderLabel((messagePageViewModel.replyViewSelectedMessage.value!).body.value) : coreViewModel.translations.chatPage.message.messagesHeadline); const title = React.createProxyState(
[messagePageViewModel.isReplyViewActive],
() =>
messagePageViewModel.isReplyViewActive.value
? coreViewModel.translations.chatPage.message.replyHeaderLabel(
messagePageViewModel.replyViewSelectedMessage.value!.body
.value,
)
: coreViewModel.translations.chatPage.message.messagesHeadline,
);
return ( return (
<div id="message-page" toggle:reply-view={isInReplyView}> <div id="message-page" toggle:reply-view={isInReplyView}>
<div class="pane-wrapper"> <div class="pane-wrapper">
<div class="pane"> <div class="pane">
@@ -88,15 +108,16 @@ return (
<button <button
class="ghost" class="ghost"
on:click={messagePageViewModel.resetReplyView} on:click={messagePageViewModel.resetReplyView}
toggle:hidden={messagePageViewModel.isReplyViewInactive} toggle:hidden={
messagePageViewModel.isReplyViewInactive
}
> >
<span class="icon">arrow_back</span> <span class="icon">arrow_back</span>
</button> </button>
<span <span
class="title ellipsis width-100 flex" class="title ellipsis width-100 flex"
subscribe:innerText={title} subscribe:innerText={title}
> ></span>
</span>
<span> <span>
<button <button
class="ghost inset-outline" class="ghost inset-outline"
@@ -166,5 +187,5 @@ return (
ChatMessageViewModelToView, ChatMessageViewModelToView,
)} )}
</div> </div>
); );
} }
+6 -13
View File
@@ -47,12 +47,8 @@ export function SettingsPage(
} }
</span> </span>
<input <input
bind:value={ bind:value={settingsPageViewModel.nameInput}
settingsPageViewModel.nameInput on:enter={settingsPageViewModel.setName}
}
on:enter={
settingsPageViewModel.setName
}
></input> ></input>
</div> </div>
</label> </label>
@@ -63,9 +59,7 @@ export function SettingsPage(
coreViewModel.translations.chatPage.settings coreViewModel.translations.chatPage.settings
.setNameButtonAudioLabel .setNameButtonAudioLabel
} }
on:click={ on:click={settingsPageViewModel.setName}
settingsPageViewModel.setName
}
toggle:disabled={ toggle:disabled={
settingsPageViewModel.cannotSetPrimaryChannel settingsPageViewModel.cannotSetPrimaryChannel
} }
@@ -186,17 +180,16 @@ export function SettingsPage(
coreViewModel.translations.chatPage.settings coreViewModel.translations.chatPage.settings
.setColorButtonAudioLabel .setColorButtonAudioLabel
} }
on:click={ on:click={settingsPageViewModel.applyColor}
settingsPageViewModel.applyColor toggle:disabled={
settingsPageViewModel.cannotSetColor
} }
toggle:disabled={settingsPageViewModel.cannotSetColor}
> >
{coreViewModel.translations.general.setButton} {coreViewModel.translations.general.setButton}
<span class="icon">check</span> <span class="icon">check</span>
</button> </button>
</div> </div>
<hr></hr> <hr></hr>
<div class="width-input"> <div class="width-input">
+8 -3
View File
@@ -2,11 +2,11 @@ import "./taskPage.css";
import { BoardPage } from "./boardPage"; import { BoardPage } from "./boardPage";
import { ViewController } from "../viewController"; import { ViewController } from "../viewController";
import { PlaceholderView } from "../Components/placeholderView"; import { PlaceholderView } from "../Components/placeholderView";
import { NewItemEntry } from "../Components/newItemEntry";
import { BoardViewModelToEntry } from "../Components/boardEntry"; import { BoardViewModelToEntry } from "../Components/boardEntry";
import * as React from "../../react"; import * as React from "../../react";
import TaskPageViewModel from "../../ViewModel/Pages/taskPageViewModel"; import TaskPageViewModel from "../../ViewModel/Pages/taskPageViewModel";
import CoreViewModel from "../../ViewModel/Global/coreViewModel"; import CoreViewModel from "../../ViewModel/Global/coreViewModel";
import {NewItemEntry} from "../Components/newItemEntry";
export function TaskPage( export function TaskPage(
coreViewModel: CoreViewModel, coreViewModel: CoreViewModel,
@@ -66,13 +66,18 @@ export function TaskPage(
bind:value={taskPageViewModel.boardQuery} bind:value={taskPageViewModel.boardQuery}
on:enter={taskPageViewModel.createBoard} on:enter={taskPageViewModel.createBoard}
placeholder={ placeholder={
coreViewModel.translations.general.filterOrCreateLabel coreViewModel.translations.general
.filterOrCreateLabel
} }
></input> ></input>
</div> </div>
</div> </div>
<div class="content gap"> <div class="content gap">
{NewItemEntry(coreViewModel, taskPageViewModel.boardQuery, taskPageViewModel.createBoard)} {NewItemEntry(
coreViewModel,
taskPageViewModel.boardQuery,
taskPageViewModel.createBoard,
)}
<div <div
class="grid gap" class="grid gap"
style="grid-template-columns: repeat(auto-fill, minmax(12rem, 1fr))" style="grid-template-columns: repeat(auto-fill, minmax(12rem, 1fr))"
+2 -6
View File
@@ -12,14 +12,10 @@ export function ChatEntry(chatViewModel: ChatViewModel) {
> >
<span <span
class="shadow" class="shadow"
subscribe:innerText={ subscribe:innerText={chatViewModel.settingsPageViewModel.name}
chatViewModel.settingsPageViewModel.name
}
></span> ></span>
<h2 <h2
subscribe:innerText={ subscribe:innerText={chatViewModel.settingsPageViewModel.name}
chatViewModel.settingsPageViewModel.name
}
></h2> ></h2>
</button> </button>
); );
+5 -2
View File
@@ -1,3 +1,4 @@
import { ReplyLink } from "./replyLink";
import { MessageReactionButtonRow } from "./messageReactionButtonRow"; import { MessageReactionButtonRow } from "./messageReactionButtonRow";
import { InlineReply } from "./inlineReply"; import { InlineReply } from "./inlineReply";
import { ChatMessageInfoModal } from "../Modals/chatMessageInfoModal"; import { ChatMessageInfoModal } from "../Modals/chatMessageInfoModal";
@@ -5,7 +6,6 @@ import * as React from "../../react";
import CoreViewModel from "../../ViewModel/Global/coreViewModel"; import CoreViewModel from "../../ViewModel/Global/coreViewModel";
import ChatMessageViewModel from "../../ViewModel/Chat/chatMessageViewModel"; import ChatMessageViewModel from "../../ViewModel/Chat/chatMessageViewModel";
import { ChatMessageStatuses } from "../../Model/Chat/chatModel"; import { ChatMessageStatuses } from "../../Model/Chat/chatModel";
import {ReplyLink} from "./replyLink";
export function ChatMessage( export function ChatMessage(
coreViewModel: CoreViewModel, coreViewModel: CoreViewModel,
@@ -70,7 +70,10 @@ export function ChatMessage(
coreViewModel.translations.chatPage.message coreViewModel.translations.chatPage.message
.replyToMessageButton .replyToMessageButton
} }
toggle:hidden={chatMessageViewModel.messagePageViewModel.isReplyViewActive} toggle:hidden={
chatMessageViewModel.messagePageViewModel
.isReplyViewActive
}
> >
<span class="icon">reply</span> <span class="icon">reply</span>
</button> </button>
+12 -4
View File
@@ -1,11 +1,18 @@
import * as React from "../../react"; import * as React from "../../react";
import CoreViewModel from "../../ViewModel/Global/coreViewModel"; import CoreViewModel from "../../ViewModel/Global/coreViewModel";
export function NewItemEntry(coreViewModel: CoreViewModel, query: React.State<string>, fn: ()=>void) { export function NewItemEntry(
const isHidden = React.createProxyState([query],()=>query.value == ""); coreViewModel: CoreViewModel,
const label = React.createProxyState([query],()=>coreViewModel.translations.general.createLabel(query.value)); query: React.State<string>,
fn: () => void,
) {
const isHidden = React.createProxyState([query], () => query.value == "");
const label = React.createProxyState([query], () =>
coreViewModel.translations.general.createLabel(query.value),
);
return <button return (
<button
class="standard slide-up" class="standard slide-up"
toggle:hidden={isHidden} toggle:hidden={isHidden}
on:click={fn} on:click={fn}
@@ -13,4 +20,5 @@ export function NewItemEntry(coreViewModel: CoreViewModel, query: React.State<st
<span subscribe:innerText={label}></span> <span subscribe:innerText={label}></span>
<span class="icon">add</span> <span class="icon">add</span>
</button> </button>
);
} }
+20 -11
View File
@@ -1,22 +1,31 @@
import * as React from "../../react"; import * as React from "../../react";
import ChatMessageViewModel from "../../ViewModel/Chat/chatMessageViewModel";
import CoreViewModel from "../../ViewModel/Global/coreViewModel"; import CoreViewModel from "../../ViewModel/Global/coreViewModel";
import ChatMessageViewModel from "../../ViewModel/Chat/chatMessageViewModel";
export function ReplyLink(coreViewModel: CoreViewModel, chatMessageViewModel: ChatMessageViewModel) { export function ReplyLink(
const isHidden = React.createProxyState([chatMessageViewModel.replyCount], ()=>chatMessageViewModel.replyCount.value == 0); coreViewModel: CoreViewModel,
chatMessageViewModel: ChatMessageViewModel,
) {
const isHidden = React.createProxyState(
[chatMessageViewModel.replyCount],
() => chatMessageViewModel.replyCount.value == 0,
);
function select() { function select() {
chatMessageViewModel.messagePageViewModel.setReplyView(chatMessageViewModel); chatMessageViewModel.messagePageViewModel.setReplyView(
chatMessageViewModel,
);
} }
return ( return (
<div <div class="reply-link" toggle:hidden={isHidden} on:click={select}>
class="reply-link" <span>
toggle:hidden={isHidden} {coreViewModel.translations.chatPage.message.replyPrefixLabel}
on:click={select} </span>
> <b
<span>{coreViewModel.translations.chatPage.message.replyPrefixLabel}</span> class="ellipsis"
<b class="ellipsis" subscribe:innerText={chatMessageViewModel.replyCount}></b> subscribe:innerText={chatMessageViewModel.replyCount}
></b>
</div> </div>
); );
} }
+32 -10
View File
@@ -1,20 +1,42 @@
import * as React from "../../react"; import * as React from "../../react";
import CoreViewModel from "../../ViewModel/Global/coreViewModel";
import MessagePageViewModel from "../../ViewModel/Pages/messagePageViewModel"; import MessagePageViewModel from "../../ViewModel/Pages/messagePageViewModel";
import CoreViewModel from "../../ViewModel/Global/coreViewModel";
export function ReplyViewHeader(coreViewModel: CoreViewModel, messagePageViewModel: MessagePageViewModel) { export function ReplyViewHeader(
const isHidden = React.createProxyState([messagePageViewModel.isReplyViewActive], ()=>!messagePageViewModel.isReplyViewActive.value); coreViewModel: CoreViewModel,
messagePageViewModel: MessagePageViewModel,
) {
const isHidden = React.createProxyState(
[messagePageViewModel.isReplyViewActive],
() => !messagePageViewModel.isReplyViewActive.value,
);
const label = React.createProxyState([messagePageViewModel.replyViewSelectedMessage], ()=>messagePageViewModel.replyViewSelectedMessage.value == undefined ? "" : coreViewModel.translations.chatPage.message.replyHeaderLabel(messagePageViewModel.replyViewSelectedMessage.value.sender)); const label = React.createProxyState(
const message = React.createProxyState([messagePageViewModel.replyViewSelectedMessage], ()=>messagePageViewModel.replyViewSelectedMessage.value == undefined ? "" : messagePageViewModel.replyViewSelectedMessage.value.body.value); [messagePageViewModel.replyViewSelectedMessage],
() =>
messagePageViewModel.replyViewSelectedMessage.value == undefined
? ""
: coreViewModel.translations.chatPage.message.replyHeaderLabel(
messagePageViewModel.replyViewSelectedMessage.value
.sender,
),
);
const message = React.createProxyState(
[messagePageViewModel.replyViewSelectedMessage],
() =>
messagePageViewModel.replyViewSelectedMessage.value == undefined
? ""
: messagePageViewModel.replyViewSelectedMessage.value.body
.value,
);
return ( return (
<div <div class="reply-header" toggle:hidden={isHidden}>
class="reply-header"
toggle:hidden={isHidden}
>
<b class="ellipsis" subscribe:innerText={label}></b> <b class="ellipsis" subscribe:innerText={label}></b>
<span class="secondary ellipsis" subscribe:innerText={message}></span> <span
class="secondary ellipsis"
subscribe:innerText={message}
></span>
<button <button
class="standard square" class="standard square"
on:click={messagePageViewModel.resetReplyView} on:click={messagePageViewModel.resetReplyView}
+14 -12
View File
@@ -11,9 +11,11 @@ export function TaskEntry(taskViewModel: TaskViewModel) {
schedule: taskViewModel.time.value || "---", schedule: taskViewModel.time.value || "---",
}; };
const className = React.createProxyState([taskViewModel.isNotNext], ()=> const className = React.createProxyState(
`${taskViewModel.isNotNext.value ? "standard" : "primary"} tile flex-no` [taskViewModel.isNotNext],
) () =>
`${taskViewModel.isNotNext.value ? "standard" : "primary"} tile flex-no`,
);
const view = ( const view = (
<button <button
@@ -24,11 +26,11 @@ export function TaskEntry(taskViewModel: TaskViewModel) {
on:dragstart={taskViewModel.dragStart} on:dragstart={taskViewModel.dragStart}
> >
<div> <div>
<span <span class="secondary" toggle:hidden={taskViewModel.isNotNext}>
class="secondary" {
toggle:hidden={taskViewModel.isNotNext} taskViewModel.coreViewModel.translations.chatPage
> .calendar.eventNext
{taskViewModel.coreViewModel.translations.chatPage.calendar.eventNext} }
<hr></hr> <hr></hr>
</span> </span>
<b <b
@@ -54,13 +56,13 @@ export function TaskEntry(taskViewModel: TaskViewModel) {
</div> </div>
</div> </div>
</button> </button>
); );
taskViewModel.index.subscribe((newIndex) => { taskViewModel.index.subscribe((newIndex) => {
view.style.order = newIndex; view.style.order = newIndex;
}); });
return view; return view;
} }
export const TaskViewModelToEntry: React.StateItemConverter<TaskViewModel> = ( export const TaskViewModelToEntry: React.StateItemConverter<TaskViewModel> = (
+13 -13
View File
@@ -1,26 +1,26 @@
import * as React from "../../react"; import * as React from "../../react";
export function UsageBar(label: string, valueLabel: React.State<string>, value: React.State<number>, maximum: React.State<number>) { export function UsageBar(
const style = React.createProxyState([value, maximum], ()=>`width: ${100*(value.value/maximum.value)}%`) label: string,
valueLabel: React.State<string>,
value: React.State<number>,
maximum: React.State<number>,
) {
const style = React.createProxyState(
[value, maximum],
() => `width: ${100 * (value.value / maximum.value)}%`,
);
return ( return (
<div <div class="surface flex-column padding gap">
class="surface flex-column padding gap"
>
<b>{label}</b> <b>{label}</b>
<div <div class="width-100 surface-alt" style="height: 18px">
class="width-100 surface-alt"
style="height: 18px"
>
<div <div
class="height-100 background-primary" class="height-100 background-primary"
set:style={style} set:style={style}
></div> ></div>
</div> </div>
<span <span class="secondary" subscribe:innerText={valueLabel}></span>
class="secondary"
subscribe:innerText={valueLabel}
></span>
</div> </div>
); );
} }
+4 -1
View File
@@ -74,7 +74,10 @@ export function ChatMessageInfoModal(
</button> </button>
<button <button
on:click={chatMessageViewModel.reply} on:click={chatMessageViewModel.reply}
toggle:disabled={chatMessageViewModel.messagePageViewModel.isReplyViewActive} toggle:disabled={
chatMessageViewModel.messagePageViewModel
.isReplyViewActive
}
> >
{ {
coreViewModel.translations.chatPage.message coreViewModel.translations.chatPage.message
+7 -4
View File
@@ -1,10 +1,10 @@
import { v4 } from "uuid"; import { v4 } from "uuid";
import { ViewController } from "../viewController"; import { ViewController } from "../viewController";
import { StringToOption } from "../Components/option"; import { StringToOption } from "../Components/option";
import { DeletableListItem } from "../Components/deletableListItem";
import * as React from "../../react"; import * as React from "../../react";
import SearchViewModel from "../../ViewModel/Utility/searchViewModel"; import SearchViewModel from "../../ViewModel/Utility/searchViewModel";
import CoreViewModel from "../../ViewModel/Global/coreViewModel"; import CoreViewModel from "../../ViewModel/Global/coreViewModel";
import {DeletableListItem} from "../Components/deletableListItem";
export function SearchModal<T>( export function SearchModal<T>(
coreViewModel: CoreViewModel, coreViewModel: CoreViewModel,
@@ -120,7 +120,9 @@ function SuggestionView<T>(
searchViewModel.search(suggestion); searchViewModel.search(suggestion);
} }
return DeletableListItem(coreViewModel, suggestion, return DeletableListItem(
coreViewModel,
suggestion,
<button <button
class="primary" class="primary"
aria-label={coreViewModel.translations.general.applyButton} aria-label={coreViewModel.translations.general.applyButton}
@@ -128,6 +130,7 @@ function SuggestionView<T>(
toggle:disabled={isApplied} toggle:disabled={isApplied}
> >
<span class="icon">check</span> <span class="icon">check</span>
</button> </button>,
, deleteSuggestion) deleteSuggestion,
);
} }
+4 -9
View File
@@ -130,16 +130,11 @@ function SettingsInfoPane(
on:click={coreViewModel.update} on:click={coreViewModel.update}
toggle:hidden={coreViewModel.noUpdateAvailable} toggle:hidden={coreViewModel.noUpdateAvailable}
> >
<span <span subscribe:innerText={coreViewModel.updateText}></span>
subscribe:innerText={coreViewModel.updateText}
></span>
<span class="icon">update</span> <span class="icon">update</span>
</button> </button>
<button <button class="standard" on:click={coreViewModel.checkUpdates}>
class="standard"
on:click={coreViewModel.checkUpdates}
>
{coreViewModel.translations.settings.checkUpdatesButton} {coreViewModel.translations.settings.checkUpdatesButton}
</button> </button>
</div> </div>
@@ -175,7 +170,7 @@ function SettingsRegionalPane(
settingsViewModel.firstDayOfWeek, settingsViewModel.firstDayOfWeek,
)} )}
</div> </div>
); );
} }
function SettingsAppearancePane( function SettingsAppearancePane(
@@ -212,5 +207,5 @@ function SettingsAppearancePane(
settingsViewModel.theme, settingsViewModel.theme,
)} )}
</div> </div>
); );
} }
+15 -4
View File
@@ -1,18 +1,24 @@
import { SplitModal } from "./splitModal"; import { SplitModal } from "./splitModal";
import { UsageBar } from "../Components/usageBar";
import { DirectoryItemList } from "../Components/directoryItemList"; import { DirectoryItemList } from "../Components/directoryItemList";
import { DangerousActionButton } from "../Components/dangerousActionButton"; import { DangerousActionButton } from "../Components/dangerousActionButton";
import * as React from "../../react"; import * as React from "../../react";
import StorageViewModel from "../../ViewModel/Global/storageViewModel"; import StorageViewModel from "../../ViewModel/Global/storageViewModel";
import CoreViewModel from "../../ViewModel/Global/coreViewModel"; import CoreViewModel from "../../ViewModel/Global/coreViewModel";
import { PATH_COMPONENT_SEPARATOR } from "../../Model/Global/storageModel"; import { PATH_COMPONENT_SEPARATOR } from "../../Model/Global/storageModel";
import {UsageBar} from "../Components/usageBar";
export function StorageModal( export function StorageModal(
coreViewModel: CoreViewModel, coreViewModel: CoreViewModel,
storageViewModel: StorageViewModel, storageViewModel: StorageViewModel,
) { ) {
const usageValueLabel = React.createProxyState([storageViewModel.occupiedSpaceMB, storageViewModel.maximumSpaceMB], ()=> const usageValueLabel = React.createProxyState(
coreViewModel.translations.storage.usageVauleLabel(storageViewModel.occupiedSpaceMB.value, storageViewModel.maximumSpaceMB.value)); [storageViewModel.occupiedSpaceMB, storageViewModel.maximumSpaceMB],
() =>
coreViewModel.translations.storage.usageVauleLabel(
storageViewModel.occupiedSpaceMB.value,
storageViewModel.maximumSpaceMB.value,
),
);
const detailView = React.createProxyState( const detailView = React.createProxyState(
[storageViewModel.selectedPath], [storageViewModel.selectedPath],
@@ -26,7 +32,12 @@ export function StorageModal(
<hr></hr> <hr></hr>
{UsageBar(coreViewModel.translations.storage.usageLabel, usageValueLabel, storageViewModel.occupiedSpaceMB, storageViewModel.maximumSpaceMB)} {UsageBar(
coreViewModel.translations.storage.usageLabel,
usageValueLabel,
storageViewModel.occupiedSpaceMB,
storageViewModel.maximumSpaceMB,
)}
<hr></hr> <hr></hr>
+10 -12
View File
@@ -141,9 +141,7 @@ export function HomePage(
on:click={coreViewModel.update} on:click={coreViewModel.update}
toggle:hidden={coreViewModel.noUpdateAvailable} toggle:hidden={coreViewModel.noUpdateAvailable}
> >
<span <span subscribe:innerText={coreViewModel.updateText}></span>
subscribe:innerText={coreViewModel.updateText}
></span>
<span class="icon">update</span> <span class="icon">update</span>
</button> </button>
@@ -158,9 +156,9 @@ export function HomePage(
</div> </div>
</div> </div>
</div> </div>
); );
const chatSection = ( const chatSection = (
<div id="chat-section"> <div id="chat-section">
<h2>{coreViewModel.translations.homePage.chatsHeadline}</h2> <h2>{coreViewModel.translations.homePage.chatsHeadline}</h2>
@@ -195,20 +193,20 @@ const chatSection = (
]} ]}
></div> ></div>
</div> </div>
); );
// methods // methods
function scrollToChat() { function scrollToChat() {
chatSection.scrollIntoView(); chatSection.scrollIntoView();
} }
// final // final
return ( return (
<article id="home-page"> <article id="home-page">
<div> <div>
{overviewSection} {overviewSection}
{chatSection} {chatSection}
</div> </div>
</article> </article>
); );
} }
+8 -4
View File
@@ -514,7 +514,8 @@ export const allTranslations: {
contentEmpty: "(leer)", contentEmpty: "(leer)",
usageLabel: "Speicher", usageLabel: "Speicher",
usageVauleLabel: (used: number, max: number) => `${used} von ${max} MB belegt`, usageVauleLabel: (used: number, max: number) =>
`${used} von ${max} MB belegt`,
path: "Pfad", path: "Pfad",
content: "Inhalt", content: "Inhalt",
@@ -592,7 +593,8 @@ export const allTranslations: {
questionReaction: "Reaktion: Fragezeichen", questionReaction: "Reaktion: Fragezeichen",
replyPrefixLabel: "Antworten: ", replyPrefixLabel: "Antworten: ",
replyHeaderLabel: (message: string) => `Antworten an "${message}"`, replyHeaderLabel: (message: string) =>
`Antworten an "${message}"`,
}, },
task: { task: {
@@ -826,7 +828,8 @@ export const allTranslations: {
contentEmpty: "(vacío)", contentEmpty: "(vacío)",
usageLabel: "Almacenamiento", usageLabel: "Almacenamiento",
usageVauleLabel: (used: number, max: number) => `${used} de ${max} MB en uso`, usageVauleLabel: (used: number, max: number) =>
`${used} de ${max} MB en uso`,
path: "Ruta", path: "Ruta",
content: "Contenido", content: "Contenido",
@@ -905,7 +908,8 @@ export const allTranslations: {
questionReaction: "Reaccion: signo de interrogación", questionReaction: "Reaccion: signo de interrogación",
replyPrefixLabel: "Respuestas: ", replyPrefixLabel: "Respuestas: ",
replyHeaderLabel: (message: string) => `Respuestas a "${message}"`, replyHeaderLabel: (message: string) =>
`Respuestas a "${message}"`,
}, },
task: { task: {
+1 -1
View File
@@ -111,5 +111,5 @@ export default class ChatListViewModel {
} }
return fullChannel; return fullChannel;
} };
} }
+22 -6
View File
@@ -17,7 +17,10 @@ export default class ChatMessageViewModel {
body: React.State<string> = new React.State(""); body: React.State<string> = new React.State("");
inlineReply: ChatMessageViewModel | undefined = undefined; inlineReply: ChatMessageViewModel | undefined = undefined;
replies: React.MapState<ChatMessageViewModel> = new React.MapState(); replies: React.MapState<ChatMessageViewModel> = new React.MapState();
replyCount: React.State<number> = React.createProxyState([this.replies], ()=>this.replies.value.size); replyCount: React.State<number> = React.createProxyState(
[this.replies],
() => this.replies.value.size,
);
status: React.State<ChatMessageStatuses | any> = new React.State<any>( status: React.State<ChatMessageStatuses | any> = new React.State<any>(
undefined, undefined,
); );
@@ -139,14 +142,20 @@ export default class ChatMessageViewModel {
} }
// reply // reply
const selectedMessage = this.messagePageViewModel.replyViewSelectedMessage.value; const selectedMessage =
this.messagePageViewModel.replyViewSelectedMessage.value;
if (selectedMessage == undefined) hideForReplyView = false; if (selectedMessage == undefined) hideForReplyView = false;
else if (selectedMessage.chatMessage.id == this.chatMessage.id) hideForReplyView = false; else if (selectedMessage.chatMessage.id == this.chatMessage.id)
else if (this.inlineReply != undefined && selectedMessage.chatMessage.id == this.inlineReply.chatMessage.id) hideForReplyView = false; hideForReplyView = false;
else if (
this.inlineReply != undefined &&
selectedMessage.chatMessage.id == this.inlineReply.chatMessage.id
)
hideForReplyView = false;
else hideForReplyView = true; else hideForReplyView = true;
this.isHidden.value = hideForReactions || hideForReplyView; this.isHidden.value = hideForReactions || hideForReplyView;
} };
// reactions // reactions
handleReaction = (reaction: ChatMessageReaction): void => { handleReaction = (reaction: ChatMessageReaction): void => {
@@ -199,7 +208,14 @@ export default class ChatMessageViewModel {
this.sentByUser = sentByUser; this.sentByUser = sentByUser;
this.loadData(); this.loadData();
React.bulkSubscribe([this.messagePageViewModel.reactionFilter, this.messagePageViewModel.replyViewSelectedMessage, this.allReactions], this.toggleHiding); React.bulkSubscribe(
[
this.messagePageViewModel.reactionFilter,
this.messagePageViewModel.replyViewSelectedMessage,
this.allReactions,
],
this.toggleHiding,
);
this.toggleHiding(); this.toggleHiding();
} }
} }
+4 -1
View File
@@ -189,7 +189,10 @@ export default class ChatViewModel extends ContextHost<ChatPageTypes> {
this.messagePageViewModel.handleReaction(reaction); this.messagePageViewModel.handleReaction(reaction);
}, },
); );
chatModel.changeHandlerManager.setHandler(this.chatModel.id, this.settingsPageViewModel.updateData); chatModel.changeHandlerManager.setHandler(
this.chatModel.id,
this.settingsPageViewModel.updateData,
);
// load // load
this.loadPageSelection(); this.loadPageSelection();
+24 -14
View File
@@ -9,13 +9,19 @@ import ConnectionModel from "../../Model/Global/connectionModel";
import ChatListModel from "../../Model/Chat/chatListModel"; import ChatListModel from "../../Model/Chat/chatListModel";
export default class CoreViewModel { export default class CoreViewModel {
swRegistration: ServiceWorkerRegistration|undefined = undefined; swRegistration: ServiceWorkerRegistration | undefined = undefined;
version: React.State<string> = new React.State(""); version: React.State<string> = new React.State("");
latestVersion: React.State<string> = new React.State(""); latestVersion: React.State<string> = new React.State("");
noUpdateAvailable: React.State<boolean> = React.createProxyState([this.latestVersion], () => { noUpdateAvailable: React.State<boolean> = React.createProxyState(
return (this.latestVersion.value == "" || this.latestVersion.value == this.version.value) [this.latestVersion],
}) () => {
return (
this.latestVersion.value == "" ||
this.latestVersion.value == this.version.value
);
},
);
updateText: React.State<string> = new React.State(""); updateText: React.State<string> = new React.State("");
translations: Translations; translations: Translations;
@@ -109,24 +115,27 @@ export default class CoreViewModel {
console.error(`Could not install service worker: ${error}`); console.error(`Could not install service worker: ${error}`);
} }
} }
} };
checkUpdates = (): void => { checkUpdates = (): void => {
fetch("/version").then(async response => { fetch("/version").then(async (response) => {
this.version.value = await response.text(); this.version.value = await response.text();
}); });
fetch("/latestVersion").then(async response => { fetch("/latestVersion").then(async (response) => {
this.latestVersion.value = (await response.text()).replace("\n", ""); this.latestVersion.value = (await response.text()).replace(
}) "\n",
} "",
);
});
};
update = async (): Promise<void> => { update = async (): Promise<void> => {
if (!this.swRegistration) return; if (!this.swRegistration) return;
await this.swRegistration.update(); await this.swRegistration.update();
setTimeout(()=>{ setTimeout(() => {
window.location.reload(); window.location.reload();
}, 500); }, 500);
} };
// init // init
constructor( constructor(
@@ -139,8 +148,9 @@ export default class CoreViewModel {
this.translations = this.translations =
allTranslations[settingsModel.language] || allTranslations.en; allTranslations[settingsModel.language] || allTranslations.en;
this.latestVersion.subscribe(latestVersion => { this.latestVersion.subscribe((latestVersion) => {
this.updateText.value = this.translations.homePage.updateButton(latestVersion); this.updateText.value =
this.translations.homePage.updateButton(latestVersion);
}); });
document.body.addEventListener("keydown", this.handleKeyDown); document.body.addEventListener("keydown", this.handleKeyDown);
@@ -1,13 +1,13 @@
import CoreViewModel, { Context } from "./coreViewModel"; import CoreViewModel, { Context } from "./coreViewModel";
import * as React from "../../react"; import * as React from "../../react";
import { CommonKeys } from "../../View/keystrokes"; import { CommonKeys } from "../../View/keystrokes";
import { formatISO } from "../../Model/Utility/utility";
import StorageModel, { import StorageModel, {
StorageModelSubPaths, StorageModelSubPaths,
filePaths, filePaths,
} from "../../Model/Global/storageModel"; } from "../../Model/Global/storageModel";
import { TransferData } from "../../Model/Global/fileTransferModel"; import { TransferData } from "../../Model/Global/fileTransferModel";
import ChatModel from "../../Model/Chat/chatModel"; import ChatModel from "../../Model/Chat/chatModel";
import {formatISO} from "../../Model/Utility/utility";
export default class FileTransferViewModel extends Context { export default class FileTransferViewModel extends Context {
// state // state
@@ -16,11 +16,9 @@ export default class NotificationViewModel {
if (this.seenMessageIds.has(message.id)) return; if (this.seenMessageIds.has(message.id)) return;
if (this.chatListViewModel.selectedChat.value == undefined) return; if (this.chatListViewModel.selectedChat.value == undefined) return;
const notification: Notification = const notification: Notification = this.createNotification(message);
this.createNotification(message);
const currentChat = const currentChat =
this.chatListViewModel.selectedChat.value.chatModel this.chatListViewModel.selectedChat.value.chatModel.id;
.id;
const currentPage = const currentPage =
this.chatListViewModel.selectedChat.value.selectedPage.value; this.chatListViewModel.selectedChat.value.selectedPage.value;
if ( if (
@@ -39,10 +37,7 @@ export default class NotificationViewModel {
const chat = [ const chat = [
...this.chatListViewModel.chatViewModels.value.values(), ...this.chatListViewModel.chatViewModels.value.values(),
].find( ].find((chat) => chat.chatModel.id == notification.fullChannel);
(chat) =>
chat.chatModel.id == notification.fullChannel,
);
if (!chat) return; if (!chat) return;
chat.open(); chat.open();
chat.openPage(ChatPageTypes.Messages); chat.openPage(ChatPageTypes.Messages);
+5 -3
View File
@@ -50,9 +50,11 @@ export default class StorageViewModel extends Context {
}; };
calculateUsage = (): void => { calculateUsage = (): void => {
this.occupiedSpaceMB.value = this.coreViewModel.storageModel.calculateUsage(); this.occupiedSpaceMB.value =
this.maximumSpaceMB.value = this.coreViewModel.storageModel.determineCapacity(); this.coreViewModel.storageModel.calculateUsage();
} this.maximumSpaceMB.value =
this.coreViewModel.storageModel.determineCapacity();
};
// view // view
showStorageModal = (): void => { showStorageModal = (): void => {
+6 -2
View File
@@ -7,11 +7,11 @@ import ChatViewModel, { ChatPageTypes } from "../Chat/chatViewModel";
import * as React from "../../react"; import * as React from "../../react";
import { Colors } from "../../colors"; import { Colors } from "../../colors";
import { CommonKeys } from "../../View/keystrokes"; import { CommonKeys } from "../../View/keystrokes";
import { PinchToZoomData } from "../../Model/Utility/utility";
import BoardsAndTasksModel, { import BoardsAndTasksModel, {
BoardInfoFileContent, BoardInfoFileContent,
TaskFileContent, TaskFileContent,
} from "../../Model/Files/boardsAndTasksModel"; } from "../../Model/Files/boardsAndTasksModel";
import {PinchToZoomData} from "../../Model/Utility/utility";
export default class BoardViewModel extends TaskContainingPageViewModel { export default class BoardViewModel extends TaskContainingPageViewModel {
// state // state
@@ -34,7 +34,11 @@ export default class BoardViewModel extends TaskContainingPageViewModel {
new React.ListState(); new React.ListState();
isFilterActive: React.State<boolean>; isFilterActive: React.State<boolean>;
pinchToZoomData: React.State<PinchToZoomData> = new React.State({zoom: 1, x: 0, y: 0}); pinchToZoomData: React.State<PinchToZoomData> = new React.State({
zoom: 1,
x: 0,
y: 0,
});
// paths // paths
getBasePath = (): string[] => { getBasePath = (): string[] => {
+26 -26
View File
@@ -4,11 +4,19 @@ import CoreViewModel from "../Global/coreViewModel";
import ChatViewModel, { ChatPageTypes } from "../Chat/chatViewModel"; import ChatViewModel, { ChatPageTypes } from "../Chat/chatViewModel";
import * as React from "../../react"; import * as React from "../../react";
import { CommonKeys } from "../../View/keystrokes"; import { CommonKeys } from "../../View/keystrokes";
import {
formatISO,
formatISOFromParts,
formatMonthStringFromParts,
formatTime,
isoToDateString,
isoToMonthString,
padZero,
} from "../../Model/Utility/utility";
import CalendarModel, { MonthGrid } from "../../Model/Files/calendarModel"; import CalendarModel, { MonthGrid } from "../../Model/Files/calendarModel";
import BoardsAndTasksModel, { import BoardsAndTasksModel, {
TaskFileContent, TaskFileContent,
} from "../../Model/Files/boardsAndTasksModel"; } from "../../Model/Files/boardsAndTasksModel";
import {formatISO, formatISOFromParts, formatMonthStringFromParts, formatTime, isoToDateString, isoToMonthString, padZero} from "../../Model/Utility/utility";
export const CALENDAR_EVENT_BOARD_ID = "events"; export const CALENDAR_EVENT_BOARD_ID = "events";
@@ -22,7 +30,11 @@ export default class CalendarPageViewModel extends TaskContainingPageViewModel {
} }
get selectedDateString(): string { get selectedDateString(): string {
return formatISOFromParts(this.selectedYear.value, this.selectedMonth.value, this.selectedDate.value); return formatISOFromParts(
this.selectedYear.value,
this.selectedMonth.value,
this.selectedDate.value,
);
} }
// paths // paths
@@ -40,7 +52,7 @@ export default class CalendarPageViewModel extends TaskContainingPageViewModel {
MonthGrid<React.MapState<TaskViewModel>> | undefined MonthGrid<React.MapState<TaskViewModel>> | undefined
> = new React.State<any>(undefined); > = new React.State<any>(undefined);
nextTask = new React.State<TaskViewModel|undefined>(undefined); nextTask = new React.State<TaskViewModel | undefined>(undefined);
// methods // methods
createEvent = (): void => { createEvent = (): void => {
@@ -65,9 +77,7 @@ export default class CalendarPageViewModel extends TaskContainingPageViewModel {
}; };
getEventsForDate = (): React.MapState<TaskViewModel> | undefined => { getEventsForDate = (): React.MapState<TaskViewModel> | undefined => {
const paddedDate: string = padZero( const paddedDate: string = padZero(this.selectedDate.toString(), 2);
this.selectedDate.toString(), 2
);
if (this.monthGrid.value == undefined) { if (this.monthGrid.value == undefined) {
return undefined; return undefined;
@@ -82,9 +92,7 @@ export default class CalendarPageViewModel extends TaskContainingPageViewModel {
): React.MapState<TaskViewModel> | null => { ): React.MapState<TaskViewModel> | null => {
if (this.monthGrid.value == null) return null; if (this.monthGrid.value == null) return null;
const date: string = isoToDateString( const date: string = isoToDateString(taskFileContent.date ?? "");
taskFileContent.date ?? "",
);
return this.monthGrid.value.days[date]; return this.monthGrid.value.days[date];
}; };
@@ -152,11 +160,7 @@ export default class CalendarPageViewModel extends TaskContainingPageViewModel {
}; };
handleDrop = (year: string, month: string, date: string): void => { handleDrop = (year: string, month: string, date: string): void => {
const ISOString: string = formatISOFromParts( const ISOString: string = formatISOFromParts(year, month, date);
year,
month,
date,
);
const draggedObject: any = this.coreViewModel.draggedObject.value; const draggedObject: any = this.coreViewModel.draggedObject.value;
if (draggedObject instanceof TaskViewModel == false) return; if (draggedObject instanceof TaskViewModel == false) return;
@@ -167,14 +171,11 @@ export default class CalendarPageViewModel extends TaskContainingPageViewModel {
handleChron = (): void => { handleChron = (): void => {
this.updateMonthGrid(); this.updateMonthGrid();
this.updateNextTask(); this.updateNextTask();
} };
updateMonthGrid = (): void => { updateMonthGrid = (): void => {
const today = formatISO(this.coreViewModel.todayDate.value) const today = formatISO(this.coreViewModel.todayDate.value);
if ( if (this.currentTodayDate == today) return;
this.currentTodayDate == today
)
return;
this.loadMonthTasks(); this.loadMonthTasks();
this.currentTodayDate = today; this.currentTodayDate = today;
this.selectedDate.callSubscriptions(); this.selectedDate.callSubscriptions();
@@ -188,14 +189,14 @@ export default class CalendarPageViewModel extends TaskContainingPageViewModel {
const tasks = this.getEventsForDate(); const tasks = this.getEventsForDate();
if (tasks == undefined) return; if (tasks == undefined) return;
const taskArray =[...tasks.value.values()]; const taskArray = [...tasks.value.values()];
const sorted = taskArray.sort((a, b) => a.index.value - b.index.value); const sorted = taskArray.sort((a, b) => a.index.value - b.index.value);
for (const task of sorted) { for (const task of sorted) {
if (task.time.value <= this.coreViewModel.time.value) continue; if (task.time.value <= this.coreViewModel.time.value) continue;
this.nextTask.value = task; this.nextTask.value = task;
break; break;
};
} }
};
// load // load
loadMonthTasks = (): void => { loadMonthTasks = (): void => {
@@ -217,8 +218,7 @@ export default class CalendarPageViewModel extends TaskContainingPageViewModel {
} }
}; };
loadData = (): void => { loadData = (): void => {};
};
// init // init
constructor( constructor(
@@ -237,9 +237,9 @@ export default class CalendarPageViewModel extends TaskContainingPageViewModel {
React.bulkSubscribe([this.selectedYear, this.selectedMonth], () => { React.bulkSubscribe([this.selectedYear, this.selectedMonth], () => {
this.loadMonthTasks(); this.loadMonthTasks();
}); });
this.selectedDate.subscribeSilent(()=>{ this.selectedDate.subscribeSilent(() => {
this.updateNextTask(); this.updateNextTask();
}) });
// handlers // handlers
boardsAndTasksModel.taskHandlerManager.setHandler( boardsAndTasksModel.taskHandlerManager.setHandler(
+17 -8
View File
@@ -16,9 +16,17 @@ export default class MessagePageViewModel extends Context {
new React.MapState(); new React.MapState();
filteredMessageViewModels: React.ListState<ChatMessageViewModel> = filteredMessageViewModels: React.ListState<ChatMessageViewModel> =
new React.ListState(); new React.ListState();
replyViewSelectedMessage = new React.State<ChatMessageViewModel|undefined>(undefined); replyViewSelectedMessage = new React.State<
isReplyViewActive: React.State<boolean> = React.createProxyState([this.replyViewSelectedMessage], ()=>this.replyViewSelectedMessage.value != undefined); ChatMessageViewModel | undefined
isReplyViewInactive = React.createProxyState([this.isReplyViewActive], ()=>!this.isReplyViewActive.value); >(undefined);
isReplyViewActive: React.State<boolean> = React.createProxyState(
[this.replyViewSelectedMessage],
() => this.replyViewSelectedMessage.value != undefined,
);
isReplyViewInactive = React.createProxyState(
[this.isReplyViewActive],
() => !this.isReplyViewActive.value,
);
searchViewModel: SearchViewModel<ChatMessageViewModel>; searchViewModel: SearchViewModel<ChatMessageViewModel>;
isFilterModalOpen = new React.State<boolean>(false); isFilterModalOpen = new React.State<boolean>(false);
reactionFilter = new React.State<ReactionSymbols | undefined>(undefined); reactionFilter = new React.State<ReactionSymbols | undefined>(undefined);
@@ -47,7 +55,7 @@ export default class MessagePageViewModel extends Context {
} }
}; };
sendMessageFromBody = (body: string): Promise<string|false>=> { sendMessageFromBody = (body: string): Promise<string | false> => {
let replyId: string | undefined = undefined; let replyId: string | undefined = undefined;
if (this.replyingMessage.value) { if (this.replyingMessage.value) {
replyId = this.replyingMessage.value.chatMessage.id; replyId = this.replyingMessage.value.chatMessage.id;
@@ -153,13 +161,13 @@ export default class MessagePageViewModel extends Context {
this.replyViewSelectedMessage.value = message; this.replyViewSelectedMessage.value = message;
this.revokeReactionFilter(false); this.revokeReactionFilter(false);
this.setReply(message, false); this.setReply(message, false);
} };
resetReplyView = (): void => { resetReplyView = (): void => {
this.replyViewSelectedMessage.value = undefined; this.replyViewSelectedMessage.value = undefined;
this.resetReply(false); this.resetReply(false);
this.restoreFilter(); this.restoreFilter();
} };
setFocus = (): void => { setFocus = (): void => {
this.focusSetter.callSubscriptions(); this.focusSetter.callSubscriptions();
@@ -178,8 +186,9 @@ export default class MessagePageViewModel extends Context {
restoreFilter = (): void => { restoreFilter = (): void => {
const previousFilter = this.chatViewModel.chatModel.getFilter() as any; const previousFilter = this.chatViewModel.chatModel.getFilter() as any;
if (Object.values(ReactionSymbols).includes(previousFilter)) this.setReactionFilter(previousFilter); if (Object.values(ReactionSymbols).includes(previousFilter))
} this.setReactionFilter(previousFilter);
};
// init // init
constructor( constructor(
+5 -10
View File
@@ -30,8 +30,7 @@ export default class SettingsPageViewModel extends Context {
); );
cannotSetColor: React.State<boolean> = React.createProxyState( cannotSetColor: React.State<boolean> = React.createProxyState(
[this.color, this.appliedColor], [this.color, this.appliedColor],
() => () => this.color.value == this.appliedColor.value,
this.color.value == this.appliedColor.value,
); );
cannotAddSecondaryChannel: React.State<boolean> = React.createProxyState( cannotAddSecondaryChannel: React.State<boolean> = React.createProxyState(
[this.newSecondaryChannelInput], [this.newSecondaryChannelInput],
@@ -41,11 +40,8 @@ export default class SettingsPageViewModel extends Context {
// methods // methods
setName = (): void => { setName = (): void => {
this.chatViewModel.chatModel.setName( this.chatViewModel.chatModel.setName(this.nameInput.value);
this.nameInput.value, this.name.value = this.chatViewModel.chatModel.info.name;
);
this.name.value =
this.chatViewModel.chatModel.info.name;
this.chatViewModel.chatListViewModel.updateIndices(); this.chatViewModel.chatListViewModel.updateIndices();
}; };
@@ -90,8 +86,7 @@ export default class SettingsPageViewModel extends Context {
// load // load
preloadData = (): void => { preloadData = (): void => {
this.name.value = this.name.value = this.chatViewModel.chatModel.info.name;
this.chatViewModel.chatModel.info.name;
this.color.value = this.chatViewModel.chatModel.color; this.color.value = this.chatViewModel.chatModel.color;
this.appliedColor.value = this.color.value; this.appliedColor.value = this.color.value;
@@ -101,7 +96,7 @@ export default class SettingsPageViewModel extends Context {
this.preloadData(); this.preloadData();
this.nameInput.value = this.name.value; this.nameInput.value = this.name.value;
this.chatViewModel.resetColor(); this.chatViewModel.resetColor();
} };
loadData = (): void => { loadData = (): void => {
this.nameInput.value = this.name.value; this.nameInput.value = this.name.value;
+6 -1
View File
@@ -179,7 +179,12 @@ export default class TaskPageViewModel extends ContextHost<string> {
this.chatViewModel = chatViewModel; this.chatViewModel = chatViewModel;
// filter // filter
implementFilter(this.boardViewModels, this.boardMatches, this.boardQuery, (board) => board.name.value); implementFilter(
this.boardViewModels,
this.boardMatches,
this.boardQuery,
(board) => board.name.value,
);
// context // context
this.chatViewModel.registerContext(ChatPageTypes.Tasks, this); this.chatViewModel.registerContext(ChatPageTypes.Tasks, this);
+3 -3
View File
@@ -1,4 +1,5 @@
import TaskContainingPageViewModel from "./taskContainingPageViewModel"; import TaskContainingPageViewModel from "./taskContainingPageViewModel";
import CalendarPageViewModel from "./calendarPageViewModel";
import CoreViewModel, { Context } from "../Global/coreViewModel"; import CoreViewModel, { Context } from "../Global/coreViewModel";
import ChatViewModel from "../Chat/chatViewModel"; import ChatViewModel from "../Chat/chatViewModel";
import * as React from "../../react"; import * as React from "../../react";
@@ -8,7 +9,6 @@ import { localeCompare, padZero } from "../../Model/Utility/utility";
import BoardsAndTasksModel, { import BoardsAndTasksModel, {
TaskFileContent, TaskFileContent,
} from "../../Model/Files/boardsAndTasksModel"; } from "../../Model/Files/boardsAndTasksModel";
import CalendarPageViewModel from "./calendarPageViewModel";
export default class TaskViewModel extends Context { export default class TaskViewModel extends Context {
// util // util
@@ -201,9 +201,9 @@ export default class TaskViewModel extends Context {
this.loadVersionIds(); this.loadVersionIds();
if (this.containingViewModel instanceof CalendarPageViewModel) { if (this.containingViewModel instanceof CalendarPageViewModel) {
this.containingViewModel.nextTask.subscribe((nextTask)=>{ this.containingViewModel.nextTask.subscribe((nextTask) => {
this.isNotNext.value = nextTask != this; this.isNotNext.value = nextTask != this;
}) });
} }
}; };
-1
View File
@@ -113,4 +113,3 @@ body[theme="dark"] #background-wrapper #grass-2 {
padding: var(--content-padding); padding: var(--content-padding);
} }
+4 -2
View File
@@ -80,7 +80,8 @@ body:is([theme="dark"], [theme="black"]) [color]:not([color="standard"]).tile {
background: hsl(var(--hue), var(--sat), var(--luma), 40%); background: hsl(var(--hue), var(--sat), var(--luma), 40%);
} }
body:is([theme="dark"], [theme="black"]) [color]:not([color="standard"]).background { body:is([theme="dark"], [theme="black"])
[color]:not([color="standard"]).background {
--luma: var(--background-luma-dark); --luma: var(--background-luma-dark);
} }
@@ -100,7 +101,8 @@ body [color]:not([color="standard"]).subtle-background {
background: hsl(var(--hue), 100%, var(--luma), 40%); background: hsl(var(--hue), 100%, var(--luma), 40%);
} }
body:is([theme="dark"], [theme="black"]) [color]:not([color="standard"]).subtle-background { body:is([theme="dark"], [theme="black"])
[color]:not([color="standard"]).subtle-background {
--luma: var(--subtle-luma-dark); --luma: var(--subtle-luma-dark);
} }