From a9efce21f0e76974e5ff7cc0faaca2ed686ef549 Mon Sep 17 00:00:00 2001 From: Mo Date: Fri, 28 Oct 2022 10:45:03 -0500 Subject: [PATCH] refactor: daily notes (#1901) --- .../ContentListView/Calendar/Calendar.tsx | 42 +-- .../ContentListView/Calendar/CalendarDay.tsx | 34 +- .../Calendar/InfiniteCalendar.tsx | 295 +++++++----------- .../ContentListView/Calendar/usePrevious.ts | 9 + .../ContentListView/ContentListView.tsx | 4 +- .../Daily/CreateDailySection.spec.ts | 9 + .../Daily/CreateDailySections.ts | 5 +- .../Daily/DailyContentList.tsx | 174 +++-------- .../Header/DisplayOptionsMenu.tsx | 39 +-- .../InfiniteScroller/InfiniteScroller.tsx | 209 +++++++++++++ packages/web/src/javascripts/Logging.ts | 17 + .../web/src/javascripts/Utils/DateUtils.ts | 2 +- 12 files changed, 460 insertions(+), 379 deletions(-) create mode 100644 packages/web/src/javascripts/Components/ContentListView/Calendar/usePrevious.ts create mode 100644 packages/web/src/javascripts/Components/ContentListView/Daily/CreateDailySection.spec.ts create mode 100644 packages/web/src/javascripts/Components/ContentListView/InfiniteScroller/InfiniteScroller.tsx create mode 100644 packages/web/src/javascripts/Logging.ts diff --git a/packages/web/src/javascripts/Components/ContentListView/Calendar/Calendar.tsx b/packages/web/src/javascripts/Components/ContentListView/Calendar/Calendar.tsx index 12d3a68f1..ffe11cb4c 100644 --- a/packages/web/src/javascripts/Components/ContentListView/Calendar/Calendar.tsx +++ b/packages/web/src/javascripts/Components/ContentListView/Calendar/Calendar.tsx @@ -3,7 +3,7 @@ import { CalendarActivity } from './CalendarActivity' import CalendarDay from './CalendarDay' import { CalendarDays, CalendarDaysLeap, CalendarDaysOfTheWeek } from './Constants' import { createActivityRecord, dateToDateOnlyString, isLeapYear, getStartDayOfMonth } from './CalendarUtilts' -import { isDateInSameDay } from '@/Utils/DateUtils' +import { areDatesInSameDay } from '@/Utils/DateUtils' type Props = { activities: CalendarActivity[] @@ -29,10 +29,11 @@ const Calendar: FunctionComponent = ({ activities, startDate, onDateSelec }, [startDate]) const today = new Date() - const days = isLeapYear(year) ? CalendarDaysLeap : CalendarDays + const dayBundle = isLeapYear(year) ? CalendarDaysLeap : CalendarDays + const days = Array(dayBundle[month] + (startDay - 1)).fill(null) return ( -
+
{CalendarDaysOfTheWeek.map((d) => ( @@ -42,23 +43,24 @@ const Calendar: FunctionComponent = ({ activities, startDate, onDateSelec ))}
- {Array(days[month] + (startDay - 1)) - .fill(null) - .map((_, index) => { - const d = index - (startDay - 2) - const date = new Date(year, month, d) - const activities = activityMap[dateToDateOnlyString(date)] || [] - return ( - onDateSelect(date)} - hasPendingEntry={selectedDay && isDateInSameDay(selectedDay, date)} - /> - ) - })} + {days.map((_, index) => { + const dayIndex = index - (startDay - 2) + const date = new Date(year, month, dayIndex) + const day = date.getDate() + const activities = activityMap[dateToDateOnlyString(date)] || [] + const isTemplate = selectedDay && areDatesInSameDay(selectedDay, date) + const type = activities.length > 0 ? 'item' : isTemplate ? 'template' : 'empty' + return ( + onDateSelect(date)} + type={type} + /> + ) + })}
diff --git a/packages/web/src/javascripts/Components/ContentListView/Calendar/CalendarDay.tsx b/packages/web/src/javascripts/Components/ContentListView/Calendar/CalendarDay.tsx index 7ccbeb68e..0c0d69535 100644 --- a/packages/web/src/javascripts/Components/ContentListView/Calendar/CalendarDay.tsx +++ b/packages/web/src/javascripts/Components/ContentListView/Calendar/CalendarDay.tsx @@ -1,33 +1,37 @@ import { FunctionComponent } from 'react' -import { CalendarActivity } from './CalendarActivity' type Props = { day: number - activities: CalendarActivity[] isToday: boolean onClick: () => void - hasPendingEntry?: boolean + type: 'empty' | 'item' | 'template' + isLastMonth: boolean } -const CalendarDay: FunctionComponent = ({ day, activities = [], hasPendingEntry, isToday, onClick }) => { - const hasActivity = day > 0 && activities.length > 0 - const todayClassNames = 'bg-danger text-danger-contrast font-bold' - const hasActivityClassNames = 'bg-danger-light text-danger font-bold' - const defaultClassNames = 'bg-transparent hover:bg-contrast' - const hasPendingEntryNames = 'bg-contrast' +const CalendarDay: FunctionComponent = ({ day, type, isToday, onClick, isLastMonth }) => { + let classNames = '' + if (isToday) { + classNames += 'bg-danger text-danger-contrast font-bold' + } else if (isLastMonth) { + classNames += 'text-passive-3' + } else { + if (type === 'empty') { + classNames += 'bg-transparent hover:bg-contrast' + } else if (type === 'item') { + classNames += 'bg-danger-light text-danger font-bold' + } else { + classNames += 'bg-contrast' + } + } return (
- {day > 0 ? day : ''} + {day}
) diff --git a/packages/web/src/javascripts/Components/ContentListView/Calendar/InfiniteCalendar.tsx b/packages/web/src/javascripts/Components/ContentListView/Calendar/InfiniteCalendar.tsx index 750f79af0..de119d4d3 100644 --- a/packages/web/src/javascripts/Components/ContentListView/Calendar/InfiniteCalendar.tsx +++ b/packages/web/src/javascripts/Components/ContentListView/Calendar/InfiniteCalendar.tsx @@ -1,43 +1,39 @@ -import { areDatesInSameMonth } from '@/Utils/DateUtils' -import { - forwardRef, - useCallback, - useEffect, - useImperativeHandle, - useLayoutEffect, - useMemo, - useRef, - useState, -} from 'react' +import { areDatesInSameDay, areDatesInSameMonth } from '@/Utils/DateUtils' +import { forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react' import Calendar from './Calendar' import { CalendarActivity, CalendarActivityType } from './CalendarActivity' import { CalendarMonth } from './CalendarMonth' import { CalendarMonths } from './Constants' import { insertMonths, insertMonthsWithTarget } from './CalendarUtilts' +import { InfiniteScrollerInterface, InfinteScroller } from '../InfiniteScroller/InfiniteScroller' +import { classNames } from '@/Utils/ConcatenateClassNames' +import { LoggingDomain, log } from '@/Logging' +import { usePrevious } from './usePrevious' +import { isMobileScreen } from '@/Utils' type Props = { activityType: CalendarActivityType activities: CalendarActivity[] onDateSelect: (date: Date) => void - selectedTemplateDay?: Date - selectedItemDay?: Date + selectedDay?: Date + selectedDayType?: 'item' | 'template' + className?: string } export type InfiniteCalendarInterface = { - changeMonth: (month: Date) => void + goToMonth: (month: Date) => void } -const PageSize = 10 +const PageSize = 2 const InfiniteCalendar = forwardRef( - ({ activities, onDateSelect, selectedTemplateDay, selectedItemDay }: Props, ref) => { - const [date, setDate] = useState(new Date()) - const [month, setMonth] = useState(date.getMonth()) - const [year, setYear] = useState(date.getFullYear()) - + ({ activities, onDateSelect, selectedDay, className }: Props, ref) => { const [expanded, setExpanded] = useState(true) - const [scrollWidth, setScrollWidth] = useState(0) + const [restoreScrollAfterExpand, setRestoreScrollAfterExpand] = useState(false) + const scrollerRef = useRef(null) + const previousSelectedDay = usePrevious(selectedDay) + const [activeDate, setActiveDate] = useState(new Date()) const today = new Date() const [months, setMonths] = useState(() => { const base = [{ date: today }] @@ -46,18 +42,6 @@ const InfiniteCalendar = forwardRef( return base }) - useImperativeHandle(ref, () => ({ - changeMonth(date: Date) { - setDate(date) - }, - })) - - const [firstElement, setFirstElement] = useState(null) - const [lastElement, setLastElement] = useState(null) - const [didPaginateLeft, setDidPaginateLeft] = useState(false) - const [restoreScrollAfterExpand, setRestoreScrollAfterExpand] = useState(false) - const scrollArea = useRef(null) - const hasMonthInList = useCallback( (date: Date): boolean => { for (const month of months) { @@ -74,7 +58,38 @@ const InfiniteCalendar = forwardRef( (date: Date): void => { setMonths(insertMonthsWithTarget(months, date)) }, - [months, setMonths], + [months], + ) + + const scrollToMonth = useCallback( + (date: Date) => { + const elementId = elementIdForMonth(date) + scrollerRef.current?.scrollToElementId(elementId) + }, + [scrollerRef], + ) + + const goToMonth = useCallback( + (month: Date) => { + if (!hasMonthInList(month)) { + insertMonthInList(month) + } + + log(LoggingDomain.DailyNotes, '[Calendar] Scrolling to month', month, 'from goToMonth') + setActiveDate(month) + scrollToMonth(month) + }, + [hasMonthInList, insertMonthInList, scrollToMonth], + ) + + useImperativeHandle( + ref, + () => ({ + goToMonth(date: Date) { + goToMonth(date) + }, + }), + [goToMonth], ) const resetNumberOfCalendarsToBase = useCallback( @@ -88,153 +103,63 @@ const InfiniteCalendar = forwardRef( ) useEffect(() => { - if (selectedTemplateDay) { - setDate(selectedTemplateDay) + if (selectedDay) { + if (previousSelectedDay && areDatesInSameDay(previousSelectedDay, selectedDay)) { + log(LoggingDomain.DailyNotes, '[Calendar] selectedDay has changed, but is same as previous', selectedDay) + return + } + log(LoggingDomain.DailyNotes, '[Calendar] selectedDay has changed, going to month:', selectedDay) + goToMonth(selectedDay) } - }, [selectedTemplateDay]) - - useEffect(() => { - if (selectedItemDay) { - setDate(selectedItemDay) - } - }, [selectedItemDay]) - - const scrollToMonth = useCallback((date: Date) => { - const elementId = elementIdForMonth(date) - const element = document.getElementById(elementId) - if (!element) { - return - } - - scrollArea.current!.scrollLeft = element.offsetLeft + -60 - }, []) - - useLayoutEffect(() => { - setMonth(date.getMonth()) - setYear(date.getFullYear()) - - if (!hasMonthInList(date)) { - insertMonthInList(date) - } - - scrollToMonth(date) - }, [date, hasMonthInList, insertMonthInList, scrollToMonth]) + }, [selectedDay, goToMonth, previousSelectedDay]) useEffect(() => { if (!restoreScrollAfterExpand) { return } - if (scrollArea.current && expanded) { - scrollToMonth(date) + if (expanded) { + log( + LoggingDomain.DailyNotes, + '[Calendar] Scrolling to month', + activeDate, + 'from restoreScrollAfterExpand useEffect', + ) + scrollToMonth(activeDate) setRestoreScrollAfterExpand(false) } - }, [expanded, scrollToMonth, date, restoreScrollAfterExpand, setRestoreScrollAfterExpand]) - - useLayoutEffect(() => { - if (!scrollArea.current) { - return - } - - if (didPaginateLeft) { - scrollArea.current.scrollLeft += scrollArea.current.scrollWidth - scrollWidth - setDidPaginateLeft(false) - } - }, [months, didPaginateLeft, scrollWidth]) + }, [expanded, scrollToMonth, activeDate, restoreScrollAfterExpand, setRestoreScrollAfterExpand]) const paginateLeft = useCallback(() => { - if (scrollArea.current) { - setScrollWidth(scrollArea.current.scrollWidth) - } - - const copy = months.slice() - insertMonths(copy, 'front', PageSize) - setDidPaginateLeft(true) - setMonths(copy) - }, [months, setMonths]) + log(LoggingDomain.DailyNotes, '[Calendar] paginateLeft') + setMonths((prevMonths) => { + const copy = prevMonths.slice() + insertMonths(copy, 'front', PageSize) + return copy + }) + }, [setMonths]) const paginateRight = useCallback(() => { - const copy = months.slice() - insertMonths(copy, 'end', PageSize) - setDidPaginateLeft(false) - setMonths(copy) - }, [months, setMonths]) + log(LoggingDomain.DailyNotes, '[Calendar] paginateRight') + setMonths((prevMonths) => { + const copy = prevMonths.slice() + insertMonths(copy, 'end', PageSize) + return copy + }) + }, [setMonths]) - const updateCurrentMonth = useCallback( - (index: number) => { - const newMonth = months[index] - setMonth(newMonth.date.getMonth()) - setYear(newMonth.date.getFullYear()) + const onElementVisibility = useCallback( + (id: string) => { + const index = months.findIndex((candidate) => elementIdForMonth(candidate.date) === id) + if (index >= 0) { + const newMonth = months[index] + log(LoggingDomain.DailyNotes, '[Calendar] Month element did become visible, setting activeDate', newMonth) + setActiveDate(newMonth.date) + } }, - [months, setMonth, setYear], + [months], ) - const visibilityObserver = useMemo( - () => - new IntersectionObserver( - (entries) => { - const visibleEntry = entries.find((entry) => entry.isIntersecting) - if (visibleEntry) { - const id = visibleEntry.target.id - const index = months.findIndex((candidate) => elementIdForMonth(candidate.date) === id) - updateCurrentMonth(index) - } - }, - { threshold: 0.9 }, - ), - [updateCurrentMonth, months], - ) - - const rightObserver = useMemo( - () => - new IntersectionObserver( - (entries) => { - if (entries[0].isIntersecting) { - paginateRight() - } - }, - { threshold: 0.5 }, - ), - [paginateRight], - ) - - const leftObserver = useMemo( - () => - new IntersectionObserver( - (entries) => { - if (entries[0].isIntersecting) { - paginateLeft() - } - }, - { threshold: 1.0 }, - ), - [paginateLeft], - ) - - useEffect(() => { - if (lastElement) { - rightObserver.observe(lastElement) - } - - return () => { - if (lastElement) { - rightObserver.unobserve(lastElement) - } - } - }, [lastElement, rightObserver]) - - useEffect(() => { - if (firstElement) { - leftObserver.observe(firstElement) - } - - return () => { - if (firstElement) { - leftObserver.unobserve(firstElement) - } - } - }, [firstElement, leftObserver]) - const toggleVisibility = useCallback(() => { setRestoreScrollAfterExpand(true) @@ -254,48 +179,42 @@ const InfiniteCalendar = forwardRef( ) return ( -
+
- {CalendarMonths[month]} {year} + {CalendarMonths[activeDate.getMonth()]} {activeDate.getFullYear()}
{expanded && ( -
- {months.map((month, index) => { - const isFirst = index === 0 - const isLast = index === months.length - 1 + {months.map((month) => { const id = elementIdForMonth(month.date) return ( -
{ - isFirst ? setFirstElement(ref) : isLast ? setLastElement(ref) : null - - if (ref) { - visibilityObserver.observe(ref) - } - }} - > +
) })} -
+ )}
) diff --git a/packages/web/src/javascripts/Components/ContentListView/Calendar/usePrevious.ts b/packages/web/src/javascripts/Components/ContentListView/Calendar/usePrevious.ts new file mode 100644 index 000000000..ca5bd896d --- /dev/null +++ b/packages/web/src/javascripts/Components/ContentListView/Calendar/usePrevious.ts @@ -0,0 +1,9 @@ +import { useEffect, useRef } from 'react' + +export const usePrevious = (value: T): T | undefined => { + const ref = useRef() + useEffect(() => { + ref.current = value + }) + return ref.current +} diff --git a/packages/web/src/javascripts/Components/ContentListView/ContentListView.tsx b/packages/web/src/javascripts/Components/ContentListView/ContentListView.tsx index 2242b661d..0ce0467ec 100644 --- a/packages/web/src/javascripts/Components/ContentListView/ContentListView.tsx +++ b/packages/web/src/javascripts/Components/ContentListView/ContentListView.tsx @@ -247,7 +247,7 @@ const ContentListView: FunctionComponent = ({
= ({ aria-label={'Notes & Files'} ref={itemsViewPanelRef} > - +
{ + it('createDailyItemsWithToday', () => { + const result = createDailyItemsWithToday(10) + + expect(result).toHaveLength(10) + }) +}) diff --git a/packages/web/src/javascripts/Components/ContentListView/Daily/CreateDailySections.ts b/packages/web/src/javascripts/Components/ContentListView/Daily/CreateDailySections.ts index a48f7cb04..ce0842f62 100644 --- a/packages/web/src/javascripts/Components/ContentListView/Daily/CreateDailySections.ts +++ b/packages/web/src/javascripts/Components/ContentListView/Daily/CreateDailySections.ts @@ -31,8 +31,9 @@ export const templateEntryForDate = (date: Date): DailyItemsDay => { } export function createDailyItemsWithToday(count: number): DailyItemsDay[] { - const today = templateEntryForDate(new Date()) - return insertBlanks([today], 'end', count) + const items = [templateEntryForDate(new Date())] + insertBlanks(items, 'front', count / 2 - 1) + return insertBlanks(items, 'end', count / 2) } /** diff --git a/packages/web/src/javascripts/Components/ContentListView/Daily/DailyContentList.tsx b/packages/web/src/javascripts/Components/ContentListView/Daily/DailyContentList.tsx index d3fc9be8f..f7de8665d 100644 --- a/packages/web/src/javascripts/Components/ContentListView/Daily/DailyContentList.tsx +++ b/packages/web/src/javascripts/Components/ContentListView/Daily/DailyContentList.tsx @@ -1,10 +1,7 @@ import { FunctionComponent, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react' -import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/Constants/Constants' import { ListableContentItem } from '../Types/ListableContentItem' import { ItemListController } from '@/Controllers/ItemList/ItemListController' import { SelectedItemsController } from '@/Controllers/SelectedItemsController' -import { ElementIds } from '@/Constants/ElementIDs' -import { classNames } from '@/Utils/ConcatenateClassNames' import { useResponsiveAppPane } from '../../ResponsivePane/ResponsivePaneProvider' import { AppPaneId } from '../../ResponsivePane/AppPaneMetadata' import { createDailyItemsWithToday, createItemsByDateMapping, insertBlanks } from './CreateDailySections' @@ -14,6 +11,9 @@ import { SNTag } from '@standardnotes/snjs' import { CalendarActivity } from '../Calendar/CalendarActivity' import { dateToDailyDayIdentifier } from './Utils' import InfiniteCalendar, { InfiniteCalendarInterface } from '../Calendar/InfiniteCalendar' +import { InfiniteScrollerInterface, InfinteScroller } from '../InfiniteScroller/InfiniteScroller' +import { LoggingDomain, log } from '@/Logging' +import { isMobileScreen } from '@/Utils' type Props = { itemListController: ItemListController @@ -36,12 +36,9 @@ const DailyContentList: FunctionComponent = ({ const [needsSelectionReload, setNeedsSelectionReload] = useState(false) const [todayItem, setTodayItem] = useState() const [selectedDay, setSelectedDay] = useState() - const [lastElement, setLastElement] = useState(null) - const [firstElement, setFirstElement] = useState(null) - const [lastScrollHeight, setLastScrollHeight] = useState(0) - const [didPaginateTop, setDidPaginateTop] = useState(false) - const scrollArea = useRef(null) const calendarRef = useRef(null) + const [lastVisibleDay, setLastVisibleDay] = useState() + const scrollerRef = useRef(null) const [dailyItems, setDailyItems] = useState(() => { return createDailyItemsWithToday(PageSize) @@ -57,6 +54,12 @@ const DailyContentList: FunctionComponent = ({ setTodayItem(dailyItems.find((item) => item.isToday) as DailyItemsDay) }, [dailyItems]) + useLayoutEffect(() => { + if (todayItem && scrollerRef.current) { + scrollerRef.current?.scrollToElementId(todayItem.id) + } + }, [todayItem, scrollerRef]) + const calendarActivities: CalendarActivity[] = useMemo(() => { return items.map((item) => { return { @@ -67,108 +70,37 @@ const DailyContentList: FunctionComponent = ({ }, [items]) const paginateBottom = useCallback(() => { - const copy = dailyItems.slice() - insertBlanks(copy, 'end', PageSize) - setDailyItems(copy) - }, [dailyItems, setDailyItems]) + log(LoggingDomain.DailyNotes, '[ContentList] paginateBottom') + setDailyItems((prev) => { + const copy = prev.slice() + insertBlanks(copy, 'end', PageSize) + return copy + }) + }, [setDailyItems]) const paginateTop = useCallback(() => { - if (scrollArea.current) { - setLastScrollHeight(scrollArea.current.scrollHeight) - } - const copy = dailyItems.slice() - insertBlanks(copy, 'front', PageSize) - setDidPaginateTop(true) - setDailyItems(copy) - }, [dailyItems, setDailyItems, setDidPaginateTop]) - - useLayoutEffect(() => { - if (!scrollArea.current) { - return - } - - if (didPaginateTop) { - scrollArea.current.scrollTop += scrollArea.current.scrollHeight - lastScrollHeight - setDidPaginateTop(false) - } - }, [didPaginateTop, lastScrollHeight]) + log(LoggingDomain.DailyNotes, '[ContentList] paginateTop') + setDailyItems((prev) => { + const copy = prev.slice() + insertBlanks(copy, 'front', PageSize) + return copy + }) + }, [setDailyItems]) const onListItemDidBecomeVisible = useCallback( (elementId: string) => { const dailyItem = dailyItems.find((candidate) => candidate.id === elementId) - if (dailyItem) { - calendarRef?.current?.changeMonth(dailyItem.date) + if (dailyItem && dailyItem !== lastVisibleDay) { + setLastVisibleDay(dailyItem) + log(LoggingDomain.DailyNotes, '[ContentList] Item did become visible for date', dailyItem.date) + calendarRef?.current?.goToMonth(dailyItem.date) + } else { + log(LoggingDomain.DailyNotes, '[ContentList] Ignoring duplicate day visibility') } }, - [dailyItems], + [dailyItems, lastVisibleDay], ) - const visibilityObserver = useMemo( - () => - new IntersectionObserver( - (entries) => { - const visibleEntry = entries.find((entry) => entry.isIntersecting) - if (visibleEntry) { - onListItemDidBecomeVisible(visibleEntry.target.id) - } - }, - { threshold: 0.9 }, - ), - [onListItemDidBecomeVisible], - ) - - const bottomObserver = useMemo( - () => - new IntersectionObserver( - (entries) => { - const first = entries[0] - if (first.isIntersecting) { - paginateBottom() - } - }, - { threshold: 0.5 }, - ), - [paginateBottom], - ) - - const topObserver = useMemo( - () => - new IntersectionObserver( - (entries) => { - const first = entries[0] - if (first.isIntersecting) { - paginateTop() - } - }, - { threshold: 0.5 }, - ), - [paginateTop], - ) - - useEffect(() => { - if (lastElement) { - bottomObserver.observe(lastElement) - } - - return () => { - if (lastElement) { - bottomObserver.unobserve(lastElement) - } - } - }, [lastElement, bottomObserver]) - - useEffect(() => { - if (firstElement) { - topObserver.observe(firstElement) - } - - return () => { - if (firstElement) { - topObserver.unobserve(firstElement) - } - } - }, [firstElement, topObserver]) - const onClickItem = useCallback( async (day: DailyItemsDay, item: ListableContentItem, userTriggered: boolean) => { await onSelect(item, userTriggered) @@ -239,29 +171,29 @@ const DailyContentList: FunctionComponent = ({ [onClickItem, onClickTemplate, dailyItemForDate, itemsByDateMapping], ) + const hasItemsOnSelectedDay = selectedDay && itemsByDateMapping[dateToDailyDayIdentifier(selectedDay)]?.length > 0 return ( <> -
- {dailyItems.map((dailyItem, index) => { - const isFirst = index === 0 - const isLast = index === dailyItems.length - 1 + {dailyItems.map((dailyItem) => { const items = itemsByDateMapping[dailyItem.id] if (items) { return items.map((item) => ( @@ -275,12 +207,6 @@ const DailyContentList: FunctionComponent = ({ hidePreview={hideNotePreview} hideTags={hideTags} onClick={() => onClickItem(dailyItem, item, true)} - ref={(ref) => { - isLast ? setLastElement(ref) : isFirst ? setFirstElement(ref) : null - if (ref) { - visibilityObserver.observe(ref) - } - }} /> )) } else { @@ -291,17 +217,11 @@ const DailyContentList: FunctionComponent = ({ id={dailyItem.id} key={dailyItem.dateKey} onClick={() => onClickTemplate(dailyItem.date)} - ref={(ref) => { - isLast ? setLastElement(ref) : isFirst ? setFirstElement(ref) : null - if (ref) { - visibilityObserver.observe(ref) - } - }} /> ) } })} -
+ ) } diff --git a/packages/web/src/javascripts/Components/ContentListView/Header/DisplayOptionsMenu.tsx b/packages/web/src/javascripts/Components/ContentListView/Header/DisplayOptionsMenu.tsx index e042bd698..94a8b20d8 100644 --- a/packages/web/src/javascripts/Components/ContentListView/Header/DisplayOptionsMenu.tsx +++ b/packages/web/src/javascripts/Components/ContentListView/Header/DisplayOptionsMenu.tsx @@ -166,12 +166,9 @@ const DisplayOptionsMenu: FunctionComponent = ({ void changePreferences({ hideEditorIcon: !preferences.hideEditorIcon }) }, [preferences, changePreferences]) - const setEntryMode = useCallback( - (mode: 'normal' | 'daily') => { - void changePreferences({ entryMode: mode }) - }, - [changePreferences], - ) + const toggleEntryMode = useCallback(() => { + void changePreferences({ entryMode: isDailyEntry ? 'normal' : 'daily' }) + }, [isDailyEntry, changePreferences]) const TabButton: FunctionComponent<{ label: string @@ -211,7 +208,10 @@ const DisplayOptionsMenu: FunctionComponent = ({

Upgrade for per-tag preferences

- Create powerful workflows and organizational layouts with per-tag display preferences. + {DailyEntryModeEnabled && + 'Create powerful workflows and organizational layouts with per-tag display preferences and the all-new Daily Notebook feature.'} + {!DailyEntryModeEnabled && + 'Create powerful workflows and organizational layouts with per-tag display preferences.'}

}
- {controlsDisabled && } + {!controlsDisabled && } @@ -376,26 +376,17 @@ const DisplayOptionsMenu: FunctionComponent = ({ {currentMode === 'tag' && DailyEntryModeEnabled && ( <> -
Entry Mode
- setEntryMode('normal')} - checked={!selectedTag.preferences?.entryMode || selectedTag.preferences?.entryMode === 'normal'} - > -
Normal
-
- - setEntryMode('daily')} + type={MenuItemType.SwitchButton} + className="py-1 hover:bg-contrast focus:bg-info-backdrop" checked={isDailyEntry} + onChange={toggleEntryMode} > -
Daily
+
+
Daily Notebook
+
Capture new notes daily with a calendar-based experience
+
)} diff --git a/packages/web/src/javascripts/Components/ContentListView/InfiniteScroller/InfiniteScroller.tsx b/packages/web/src/javascripts/Components/ContentListView/InfiniteScroller/InfiniteScroller.tsx new file mode 100644 index 000000000..f39949b2c --- /dev/null +++ b/packages/web/src/javascripts/Components/ContentListView/InfiniteScroller/InfiniteScroller.tsx @@ -0,0 +1,209 @@ +import { + forwardRef, + ReactNode, + useCallback, + useEffect, + useImperativeHandle, + useLayoutEffect, + useMemo, + useRef, + useState, +} from 'react' + +import { LoggingDomain, log } from '@/Logging' + +type Props = { + children: ReactNode + paginateFront: () => void + paginateEnd: () => void + direction: 'horizontal' | 'vertical' + onElementVisibility?: (elementId: string) => void + className?: string + isMobileScreen?: boolean +} + +export type InfiniteScrollerInterface = { + scrollToElementId: (id: string) => void +} + +export const InfinteScroller = forwardRef( + ( + { + children, + paginateFront, + paginateEnd, + direction = 'vertical', + onElementVisibility, + className, + isMobileScreen, + }: Props, + ref, + ) => { + const frontSentinel = useRef(null) + const endSentinel = useRef(null) + const [ignoreFirstFrontSentinelEvent, setIgnoreFirstFrontSentinelEvent] = useState(true) + const [needsMobilePaginationFix, setNeedsMobilePaginationFix] = useState(false) + + const scrollArea = useRef(null) + const [scrollSize, setScrollSize] = useState(0) + const [didPaginateFront, setDidPaginateFront] = useState(false) + + useImperativeHandle(ref, () => ({ + scrollToElementId(id: string) { + scrollToElementId(id) + }, + })) + + const visibilityObserver = useMemo( + () => + new IntersectionObserver( + (entries) => { + const visibleEntry = entries.find((entry) => entry.isIntersecting) + if (visibleEntry) { + onElementVisibility?.(visibleEntry.target.id) + } + }, + { threshold: 1.0 }, + ), + [onElementVisibility], + ) + + useEffect(() => { + const childElements = Array.from(scrollArea.current!.children) + for (const child of childElements) { + visibilityObserver.observe(child) + } + + return () => { + for (const child of childElements) { + visibilityObserver.unobserve(child) + } + } + }, [visibilityObserver, children]) + + const scrollToElementId = useCallback((id: string) => { + const element = document.getElementById(id) + if (!element) { + log(LoggingDomain.DailyNotes, 'Element not found', id) + return + } + + log(LoggingDomain.DailyNotes, 'Scrolling to element', id) + element.scrollIntoView({ + behavior: 'auto', + block: 'center', + inline: 'center', + }) + }, []) + + useLayoutEffect(() => { + if (!scrollArea.current) { + return + } + + if (didPaginateFront) { + if (direction === 'vertical') { + scrollArea.current.scrollTop += scrollArea.current.scrollHeight - scrollSize + if (isMobileScreen) { + setNeedsMobilePaginationFix(true) + } + } else { + scrollArea.current.scrollLeft += scrollArea.current.scrollWidth - scrollSize + } + setDidPaginateFront(false) + } + }, [didPaginateFront, scrollSize, direction, isMobileScreen]) + + useLayoutEffect(() => { + /** + * iOS Safari has an issue rendering paginated items from the top where the new + * scrolled to area is white until the user interacts with scroll again. The fix + * we apply is to re-set scrollTop to its same value to trigger a refresh. + * https://stackoverflow.com/questions/9807620 + */ + if (needsMobilePaginationFix) { + setTimeout(() => { + if (!scrollArea.current) { + return + } + + log(LoggingDomain.DailyNotes, '[InfiniteScroller] Applying mobile pagination fix') + scrollArea.current.scrollTop += scrollArea.current.scrollHeight - scrollSize + setNeedsMobilePaginationFix(false) + }, 50) + } + }, [needsMobilePaginationFix, scrollSize]) + + const _paginateFront = useCallback(() => { + if (direction === 'vertical') { + setScrollSize(scrollArea!.current!.scrollHeight) + } else { + setScrollSize(scrollArea!.current!.scrollWidth) + } + setDidPaginateFront(true) + paginateFront() + }, [paginateFront, direction]) + + const _paginateEnd = useCallback(() => { + paginateEnd() + }, [paginateEnd]) + + const frontObserver = useMemo( + () => + new IntersectionObserver( + (entries) => { + if (entries[0].isIntersecting) { + if (ignoreFirstFrontSentinelEvent) { + log(LoggingDomain.DailyNotes, '[InfiniteScroller] Ignoring first front sentinel event') + setIgnoreFirstFrontSentinelEvent(false) + return + } + _paginateFront() + } + }, + { threshold: 0.5 }, + ), + [_paginateFront, ignoreFirstFrontSentinelEvent], + ) + + useEffect(() => { + if (frontSentinel.current) { + frontObserver.observe(frontSentinel.current) + } + }, [frontObserver, frontSentinel]) + + const endObserver = useMemo( + () => + new IntersectionObserver( + (entries) => { + if (entries[0].isIntersecting) { + _paginateEnd() + } + }, + { threshold: 0.5 }, + ), + [_paginateEnd], + ) + + useEffect(() => { + if (endSentinel.current) { + endObserver.observe(endSentinel.current) + } + }, [endObserver, endSentinel]) + + return ( +
+
+ {children} +
+
+ ) + }, +) diff --git a/packages/web/src/javascripts/Logging.ts b/packages/web/src/javascripts/Logging.ts new file mode 100644 index 000000000..f0613f54a --- /dev/null +++ b/packages/web/src/javascripts/Logging.ts @@ -0,0 +1,17 @@ +import { log as utilsLog } from '@standardnotes/utils' + +export enum LoggingDomain { + DailyNotes, +} + +const LoggingStatus: Record = { + [LoggingDomain.DailyNotes]: false, +} + +export function log(domain: LoggingDomain, ...args: any[]): void { + if (!LoggingStatus[domain]) { + return + } + + utilsLog(LoggingDomain[domain], ...args) +} diff --git a/packages/web/src/javascripts/Utils/DateUtils.ts b/packages/web/src/javascripts/Utils/DateUtils.ts index 8b8d637d7..b85e7d74c 100644 --- a/packages/web/src/javascripts/Utils/DateUtils.ts +++ b/packages/web/src/javascripts/Utils/DateUtils.ts @@ -33,7 +33,7 @@ export function numHoursBetweenDates(date1: Date, date2: Date): number { return Math.abs(date1.getTime() - date2.getTime()) / 3600000 } -export function isDateInSameDay(date1: Date, date2: Date): boolean { +export function areDatesInSameDay(date1: Date, date2: Date): boolean { return date1.toLocaleDateString() === date2.toLocaleDateString() }