From be2867e1f5906a2050d064dd5434792865e18828 Mon Sep 17 00:00:00 2001
From: Ryan Sandoval <ryan_sandoval@live.com>
Date: Sat, 3 Feb 2024 14:10:32 -0800
Subject: [PATCH 1/3] Add setting for week start day

---
 src/constants/Settings.ts |  6 ++++++
 src/index.ts              |  6 +++---
 src/settings.ts           | 25 ++++++++++++++++++++++++-
 3 files changed, 33 insertions(+), 4 deletions(-)

diff --git a/src/constants/Settings.ts b/src/constants/Settings.ts
index 8d1ec3e..e8899a8 100644
--- a/src/constants/Settings.ts
+++ b/src/constants/Settings.ts
@@ -1,2 +1,8 @@
 export const SHOW_CALENDAR_BUTTON = "showCalendarToggleOnToolbar";
 export const SHOW_MODIFIED_NOTES = "showModifiedNotes";
+export const WEEK_START_DAY = "weekStartDay";
+
+export enum WeekStartDay {
+  Sunday = "Sunday",
+  Monday = "Monday",
+}
diff --git a/src/index.ts b/src/index.ts
index 6650e3e..08ca1e6 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -7,9 +7,9 @@ import {
   registerSettings,
   triggerAllSettingsCallbacks,
   onSettingChange,
-  onSettingChangeAlertPanel,
+  registerPanelAlertOnSettingChange,
 } from "./settings";
-import { SHOW_CALENDAR_BUTTON, SHOW_MODIFIED_NOTES } from "@constants/Settings";
+import { SHOW_CALENDAR_BUTTON } from "@constants/Settings";
 
 joplin.plugins.register({
   onStart: async function () {
@@ -33,7 +33,7 @@ joplin.plugins.register({
       }
     });
 
-    await onSettingChangeAlertPanel(panel, SHOW_MODIFIED_NOTES);
+    await registerPanelAlertOnSettingChange(panel);
 
     await triggerAllSettingsCallbacks();
   },
diff --git a/src/settings.ts b/src/settings.ts
index 3790730..4340138 100644
--- a/src/settings.ts
+++ b/src/settings.ts
@@ -1,7 +1,12 @@
 import joplin from "api";
 import { SettingItemType } from "api/types";
 import MsgType from "@constants/messageTypes";
-import { SHOW_CALENDAR_BUTTON, SHOW_MODIFIED_NOTES } from "@constants/Settings";
+import {
+  SHOW_CALENDAR_BUTTON,
+  SHOW_MODIFIED_NOTES,
+  WEEK_START_DAY,
+  WeekStartDay,
+} from "@constants/Settings";
 
 const SETTINGS_SECTION_ID = "joplinCalendarSection";
 
@@ -32,6 +37,16 @@ export async function registerSettings() {
       value: true,
       section: SETTINGS_SECTION_ID,
     },
+    [WEEK_START_DAY]: {
+      label: "Week Start Day",
+      description: "Which day the week starts on",
+      public: true,
+      isEnum: true,
+      type: SettingItemType.String,
+      options: WeekStartDay,
+      value: WeekStartDay.Sunday,
+      section: SETTINGS_SECTION_ID,
+    },
   });
 
   await joplin.settings.onChange(async ({ keys }) => {
@@ -43,6 +58,14 @@ export async function registerSettings() {
   });
 }
 
+/**
+ * Alerts panel when certain settings change.
+ */
+export async function registerPanelAlertOnSettingChange(panelHandle: string) {
+  await onSettingChangeAlertPanel(panelHandle, SHOW_MODIFIED_NOTES);
+  await onSettingChangeAlertPanel(panelHandle, WEEK_START_DAY);
+}
+
 export async function triggerAllSettingsCallbacks() {
   Object.entries(settingObservers).forEach(async ([key, callbacks]) => {
     callbacks.forEach(async (callback) => {

From e181be4d036378c5fef634a0081d01694c7c2e2c Mon Sep 17 00:00:00 2001
From: Ryan Sandoval <ryan_sandoval@live.com>
Date: Sat, 3 Feb 2024 14:31:34 -0800
Subject: [PATCH 2/3] Implement an onSettingsChange hook

---
 src/gui/hooks/useNoteSearchTypes.ts  | 33 +++----------------
 src/gui/hooks/useOnSettingsChange.ts | 48 ++++++++++++++++++++++++++++
 2 files changed, 53 insertions(+), 28 deletions(-)
 create mode 100644 src/gui/hooks/useOnSettingsChange.ts

diff --git a/src/gui/hooks/useNoteSearchTypes.ts b/src/gui/hooks/useNoteSearchTypes.ts
index dcbea79..3274935 100644
--- a/src/gui/hooks/useNoteSearchTypes.ts
+++ b/src/gui/hooks/useNoteSearchTypes.ts
@@ -3,6 +3,7 @@ import MsgType from "@constants/messageTypes";
 import { useEffect, useState } from "react";
 import useWebviewApiOnMessage from "./useWebViewApiOnMessage";
 import { SHOW_MODIFIED_NOTES } from "@constants/Settings";
+import useOnSettingsChange from "./useOnSettingsChange";
 
 /**
  * Provides note types to search for when fetching notes, based on user preference.
@@ -10,38 +11,14 @@ import { SHOW_MODIFIED_NOTES } from "@constants/Settings";
  * Note type examples: Created Notes, Modified Notes
  */
 function useNoteSearchTypes() {
-  const [showModifiedNotes, setShowModifiedNotes] = useState(false);
+  const showModifiedNotes = useOnSettingsChange<boolean>(
+    SHOW_MODIFIED_NOTES,
+    false
+  );
   const noteSearchTypes = [NoteSearchTypes.Created];
   if (showModifiedNotes) {
     noteSearchTypes.push(NoteSearchTypes.Modified);
   }
-
-  // Trigger all settings callbacks once initialized
-  // to prevent any race conditions.
-  useEffect(() => {
-    webviewApi.postMessage({
-      type: MsgType.TriggerAllSettingsCallbacks,
-    });
-  }, []);
-
-  useWebviewApiOnMessage((data) => {
-    const message = data.message;
-
-    if (!message.type) {
-      return;
-    }
-    if (message.type !== MsgType.SettingChanged) {
-      return;
-    }
-
-    const settingMessage = message as any;
-
-    if (settingMessage.key !== SHOW_MODIFIED_NOTES) {
-      return;
-    }
-    setShowModifiedNotes((message as any).value);
-  });
-
   return noteSearchTypes;
 }
 
diff --git a/src/gui/hooks/useOnSettingsChange.ts b/src/gui/hooks/useOnSettingsChange.ts
new file mode 100644
index 0000000..0b36bd1
--- /dev/null
+++ b/src/gui/hooks/useOnSettingsChange.ts
@@ -0,0 +1,48 @@
+import MsgType from "@constants/messageTypes";
+import { useEffect, useState } from "react";
+import useWebviewApiOnMessage from "./useWebViewApiOnMessage";
+
+/**
+ * Providers the settings value for the given settings key.
+ *
+ * @template SettingType The type of the settings value
+ *
+ * @param settingKey The settings key
+ * @returns The settings value
+ */
+function useOnSettingsChange<SettingType>(
+  settingKey: string,
+  defaultValue: SettingType = null
+): SettingType {
+  const [settingValue, setSettingValue] = useState<SettingType>(defaultValue);
+
+  // Trigger all settings callbacks once initialized
+  // to prevent any race conditions.
+  useEffect(() => {
+    webviewApi.postMessage({
+      type: MsgType.TriggerAllSettingsCallbacks,
+    });
+  }, []);
+
+  useWebviewApiOnMessage((data) => {
+    const message = data.message;
+
+    if (!message.type) {
+      return;
+    }
+    if (message.type !== MsgType.SettingChanged) {
+      return;
+    }
+
+    const settingMessage = message as any;
+
+    if (settingMessage.key !== settingKey) {
+      return;
+    }
+    setSettingValue((message as any).value);
+  });
+
+  return settingValue;
+}
+
+export default useOnSettingsChange;

From 01453d7f1b01b0fd9b080c0bb33ae99a13bc6f66 Mon Sep 17 00:00:00 2001
From: Ryan Sandoval <ryan_sandoval@live.com>
Date: Sat, 3 Feb 2024 15:40:06 -0800
Subject: [PATCH 3/3] Add support to start week on Monday

---
 src/gui/Calendar/CalendarHeader.tsx          | 13 +++++++++
 src/gui/Calendar/__tests__/Calendar.test.tsx | 28 ++++++++++++++++++++
 src/gui/Calendar/index.tsx                   | 27 +++++++++++++++++--
 3 files changed, 66 insertions(+), 2 deletions(-)

diff --git a/src/gui/Calendar/CalendarHeader.tsx b/src/gui/Calendar/CalendarHeader.tsx
index 5604fa1..b7f966b 100644
--- a/src/gui/Calendar/CalendarHeader.tsx
+++ b/src/gui/Calendar/CalendarHeader.tsx
@@ -1,6 +1,8 @@
 import { weekdaysShort } from "moment";
 import React from "react";
 import styled from "styled-components";
+import useOnSettingsChange from "../hooks/useOnSettingsChange";
+import { WEEK_START_DAY, WeekStartDay } from "@constants/Settings";
 
 const HeaderCell = styled.th`
   font-size: var(--joplin-font-size);
@@ -19,6 +21,17 @@ function CalendarHeader(props: CalendarHeaderProps) {
   const calendarHeader = weekdaysShort().map((day) => (
     <HeaderCell>{day}</HeaderCell>
   ));
+
+  const weekStartDay = useOnSettingsChange<WeekStartDay>(
+    WEEK_START_DAY,
+    WeekStartDay.Sunday
+  );
+
+  // Need to shift the headers based on the week start day
+  if (weekStartDay === WeekStartDay.Monday) {
+    calendarHeader.push(calendarHeader.shift());
+  }
+
   return <HeaderRow>{...calendarHeader}</HeaderRow>;
 }
 
diff --git a/src/gui/Calendar/__tests__/Calendar.test.tsx b/src/gui/Calendar/__tests__/Calendar.test.tsx
index 39d6db4..e433974 100644
--- a/src/gui/Calendar/__tests__/Calendar.test.tsx
+++ b/src/gui/Calendar/__tests__/Calendar.test.tsx
@@ -6,10 +6,15 @@ import moment from "moment";
 import { act } from "react-dom/test-utils";
 import useGetMonthStatistics from "../../hooks/useGetMonthStatistics";
 import useWebviewApiOnMessage from "../../hooks/useWebViewApiOnMessage";
+import { WeekStartDay } from "@constants/Settings";
+import useOnSettingsChange from "../../hooks/useOnSettingsChange";
 
 jest.mock("../../hooks/useGetMonthStatistics");
 const mockedUseGetMonthStatistics = jest.mocked(useGetMonthStatistics);
 
+jest.mock("../../hooks/useOnSettingsChange");
+const mockedUseOnSettingsChange = jest.mocked(useOnSettingsChange);
+
 global.webviewApi = {
   postMessage: jest.fn(),
   onMessage: jest.fn(),
@@ -26,6 +31,7 @@ describe("calendar", () => {
       },
       refetch: jest.fn(),
     });
+    mockedUseOnSettingsChange.mockReset();
   });
 
   it("displays dates correctly", () => {
@@ -51,6 +57,28 @@ describe("calendar", () => {
     }
   });
 
+  it("displays dates correctly if week starts on Monday", () => {
+    mockedUseOnSettingsChange.mockReturnValue(WeekStartDay.Monday);
+
+    const date = moment("May-29-2023", "MMM-DD-YYYY");
+    render(<Calendar selectedDate={date} />);
+
+    expect(screen.getByText("May 2023")).toBeDefined();
+
+    const cells = screen.getAllByRole("cell");
+    expect(cells).toHaveLength(42); // 7 days * 6 rows
+
+    // Assert May (No April)
+    for (let i = 0; i <= 30; i++) {
+      expect(cells[i].textContent).toEqual((i + 1).toString());
+    }
+
+    // Assert June
+    for (let i = 1; i <= 11; i++) {
+      expect(cells[i + 30].textContent).toEqual(i.toString());
+    }
+  });
+
   it("calls callback when next month clicked", () => {
     const nextMonthCallback = jest.fn();
     const date = moment("May-29-2023", "MMM-DD-YYYY");
diff --git a/src/gui/Calendar/index.tsx b/src/gui/Calendar/index.tsx
index 375ea76..1868654 100644
--- a/src/gui/Calendar/index.tsx
+++ b/src/gui/Calendar/index.tsx
@@ -15,6 +15,8 @@ import useGetMonthStatistics from "../hooks/useGetMonthStatistics";
 import MsgType from "@constants/messageTypes";
 import { PluginPostMessage } from "@constants/pluginMessageTypes";
 import useWebviewApiOnMessage from "../hooks/useWebViewApiOnMessage";
+import useOnSettingsChange from "../hooks/useOnSettingsChange";
+import { WEEK_START_DAY, WeekStartDay } from "@constants/Settings";
 
 const DAYS_IN_A_WEEK = 7;
 const CALENDAR_ROWS = 6;
@@ -78,16 +80,37 @@ function Calendar({
     [onKeyboardNavigation]
   );
 
+  const weekStartDay = useOnSettingsChange<WeekStartDay>(
+    WEEK_START_DAY,
+    WeekStartDay.Sunday
+  );
+
   const currentMonthFirstDay = shownMonth.startOf("month");
 
   const calendarBody: React.JSX.Element[] = [];
-  const firstRowOffset = -currentMonthFirstDay.weekday();
+  let firstRowBacktrackOffset;
+  if (weekStartDay === WeekStartDay.Monday) {
+    // If the current month starts on a day other than Monday, we need to backtrack.
+    // Monday has isoWeek 1, and we need to  backtrack 0 days.
+    // Tuesday has isoWeek 2, and we need to backtrack 1 days.
+    // ...
+    // Sunday has isoWeek 7, and we need to backtrack 6 days.
+    firstRowBacktrackOffset = currentMonthFirstDay.isoWeekday() - 1;
+  } else {
+    // Fallback to asssuming week starts on Sunday.
+    // If current month starts on a day other than Sunday, we need to backtrack.
+    // Monday has isoWeek 1, and we need to  backtrack 1 day.
+    // Tuesday has isoWeek 2, and we need to backtrack 2 days.
+    // ...
+    // Sunday has isoWeek 7, and we need to backtrack no days.
+    firstRowBacktrackOffset = currentMonthFirstDay.isoWeekday() % 7;
+  }
 
   // Note: Moment JS uses in place operations
   const workingDate = currentMonthFirstDay.clone();
 
   // Offset to fill in dates from previous month till first of current month.
-  workingDate.add(firstRowOffset, "days");
+  workingDate.subtract(firstRowBacktrackOffset, "days");
 
   for (let row = 0; row < CALENDAR_ROWS; row++) {
     const cols: React.JSX.Element[] = [];