Skip to content

Commit

Permalink
WIP: When friendly dates are enabled, group elements use friendly dat…
Browse files Browse the repository at this point in the history
…es (#717)

* Adds missing `import` statement

* Gentle refactoring in `updateAttributes()`

* Removes redundant assignment of `attributes` to itself

In more detail, it assigns to `attributes` a new Object whose keys are
all the values of `sorting[i].value`.

These values should be all the possible attributes, so in effect, the
keys don't change.

The value assigned to each of these keys is `attributes[key]`, in other
words, the value which that key **already has** in `attributes`.

So, `attributes` should not change. So the line is redundant.

As it prevents `attributes` from being made `const`, I've removed it.

* Const-ifies `attributes`

* Adds AttributeKey and DateAttributeKey types

Also gently refactors updateAttributes() to take
advantage of the stronger typing

* Adds FriendlyDateGroup type

* Refactors friendlyDate()

Separates out the step which find the right
translation key for the friendly date from the
stage which translates that key.

In particular, adds a new function,
`friendlyDateTranslationKey()`, which just finds
the right translation key. `friendlyDate()` now
just calls the first function and applies the
translation function from i18next.

* Defines `friendlyDateGroup()`

* Refactors DateAttribute and DateAttributeKey types

* Defines `getDateAttributes()`

This will be useful for grouping by friendly date,
since we need to identify whether the attribute
key associated with a given group is a date
attribute key.

* Groups date attributes by friendly date translation key when friendly dates are enabled.

As before, when the user hasn't got friendly dates
enabled, todo objects will be grouped by the raw
value of date attributes, hopefully an ISO string
like `2024-07-10`.

But when the user enables friendly dates, todos
will instead be grouped by the friendly date
translation key, such as `drawer.attributes.today`.

* Group titles in UI use friendly dates when enabled

---------

Co-authored-by: Joe Carstairs <me@joeac.net>
Co-authored-by: ransome <11188741+ransome1@users.noreply.github.com>
  • Loading branch information
3 people authored Aug 4, 2024
1 parent f975507 commit c3fceea
Show file tree
Hide file tree
Showing 7 changed files with 197 additions and 98 deletions.
48 changes: 29 additions & 19 deletions src/main/modules/Attributes.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,24 @@
let attributes: Attributes = {
const attributes: Attributes = {
priority: {},
projects: {},
contexts: {},
due: {},
t: {},
due: {} as DateAttribute,
t: {} as DateAttribute,
rec: {},
pm: {},
created: {},
completed: {},
created: {} as DateAttribute,
completed: {} as DateAttribute,
};

function getDateAttributes(): DateAttributes {
return {
due: attributes.due,
t: attributes.t,
created: attributes.created,
completed: attributes.completed,
};
}

function incrementCount(countObject: any, key: any | null, notify: boolean): void {
if(key) {
let previousCount: number = parseInt(countObject[key]?.count) || 0;
Expand All @@ -21,34 +30,35 @@ function incrementCount(countObject: any, key: any | null, notify: boolean): voi
}

function updateAttributes(todoObjects: TodoObject[], sorting: Sorting[], reset: boolean) {

Object.keys(attributes).forEach((key) => {

Object.keys(attributes[key]).forEach((attributeKey) => {

const attributeKeys = Object.keys(attributes) as AttributeKey[];

for (const key of attributeKeys) {

for (const attributeKey in attributes[key]) {
(reset) ? attributes[key] = {} : attributes[key][attributeKey].count = 0
});
};

todoObjects.forEach((todoObject: TodoObject) => {
for (const todoObject of todoObjects) {
const value = todoObject[key as keyof TodoObject];
const notify: boolean = (key === 'due') ? !!todoObject?.notify : false;

if(Array.isArray(value)) {
value.forEach((element) => {
for (const element of value) {
if(element !== null) {
const attributeKey = element as keyof Attribute;

incrementCount(attributes[key], attributeKey, notify);
}
});
}
} else {
if(value !== null) {
incrementCount(attributes[key], value, notify);
}
}
});
}
attributes[key] = Object.fromEntries(Object.entries(attributes[key]).sort(([a], [b]) => a.localeCompare(b)));
});
attributes = Object.fromEntries(sorting.map((item) => [item.value, attributes[item.value]]));
}
}

export { attributes, updateAttributes };
export { attributes, getDateAttributes, updateAttributes };
14 changes: 7 additions & 7 deletions src/main/modules/DataRequest/CreateTodoObjects.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,12 @@ function createTodoObject(lineNumber: number, string: string, attributeType?: st
content = JsTodoTxtObject.toString().replaceAll(' [LB] ', String.fromCharCode(16));

const body = JsTodoTxtObject.body().replaceAll(' [LB] ', ' ');
const speakingDates: DateAttributes = extractSpeakingDates(body);
const due = speakingDates['due:']?.date || null;
const dueString = speakingDates['due:']?.string || null;
const notify = speakingDates['due:']?.notify || false;
const t = speakingDates['t:']?.date || null;
const tString = speakingDates['t:']?.string || null;
const speakingDates = extractSpeakingDates(body);
const due = speakingDates.due?.date || null;
const dueString = speakingDates.due?.string || null;
const notify = speakingDates.due?.notify || false;
const t = speakingDates.t?.date || null;
const tString = speakingDates.t?.string || null;
const hidden = extensions.some(extension => extension.key === 'h' && extension.value === '1');
const pm: string | number | null = extensions.find(extension => extension.key === 'pm')?.value || null;
const rec = extensions.find(extension => extension.key === 'rec')?.value || null;
Expand Down Expand Up @@ -97,4 +97,4 @@ function createTodoObjects(fileContent: string | null): TodoObject[] | [] {
return todoObjects;
}

export { createTodoObjects, createTodoObject, linesInFile };
export { createTodoObjects, createTodoObject, linesInFile };
19 changes: 16 additions & 3 deletions src/main/modules/DataRequest/SortAndGroup.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { config } from '../../config';
import { getDateAttributes } from '../Attributes';
import { friendlyDateGroup } from '../Date';

function sortAndGroupTodoObjects(todoObjects: TodoObject[], sorting: Sorting[]): TodoGroup {
const fileSorting: boolean = config.get('fileSorting');
Expand Down Expand Up @@ -31,13 +33,24 @@ function sortAndGroupTodoObjects(todoObjects: TodoObject[], sorting: Sorting[]):
return 0;
}

function getGroupKey(todoObject: TodoObject, attributeKey: string) {
const useFriendlyDates = config.get('useHumanFriendlyDates');
const isDateAttribute = Object.keys(getDateAttributes()).includes(attributeKey);

if (useFriendlyDates && isDateAttribute) {
return friendlyDateGroup(todoObject[attributeKey]);
}

return todoObject[attributeKey];
}

function groupTodoObjectsByKey(todoObjects: TodoObject[], attributeKey: string) {
const grouped: TodoGroup = {};
for (const todoObject of todoObjects) {
const groupKey = todoObject[attributeKey] || null;
const groupKey = getGroupKey(todoObject, attributeKey);
if (!grouped[groupKey]) {
grouped[groupKey] = {
title: groupKey,
title: todoObject[attributeKey],
todoObjects: [],
visible: false
};
Expand Down Expand Up @@ -70,4 +83,4 @@ function sortAndGroupTodoObjects(todoObjects: TodoObject[], sorting: Sorting[]):
return sortedAndGroupedTodoObjects;
}

export { sortAndGroupTodoObjects };
export { sortAndGroupTodoObjects };
78 changes: 64 additions & 14 deletions src/main/modules/Date.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import Sugar from 'sugar';
import dayjs from 'dayjs';
import dayjs, { Dayjs } from 'dayjs';
import { config } from '../config';

function mustNotify(date: Date): boolean {
Expand All @@ -9,9 +9,9 @@ function mustNotify(date: Date): boolean {
}

function replaceSpeakingDatesWithAbsoluteDates(string: string): string {
const speakingDates: DateAttributes = extractSpeakingDates(string);
const due: DateAttribute = speakingDates['due:'];
const t: DateAttribute = speakingDates['t:'];
const speakingDates = extractSpeakingDates(string);
const due: DateAttribute = speakingDates.due;
const t: DateAttribute = speakingDates.t;
if(due.date) {
string = string.replace(due.string!, due.date);
}
Expand Down Expand Up @@ -50,22 +50,22 @@ function processDateWithSugar(string: string, type: string): DateAttribute | nul
return lastMatch;
}

function extractSpeakingDates(body: string): DateAttributes {
const expressions = [
{ pattern: /due:(?!(\d{4}-\d{2}-\d{2}))(.*?)(?=t:|$)/g, key: 'due:', type: 'relative' },
{ pattern: /due:(\d{4}-\d{2}-\d{2})/g, key: 'due:', type: 'absolute' },
{ pattern: /t:(?!(\d{4}-\d{2}-\d{2}))(.*?)(?=due:|$)/g, key: 't:', type: 'relative' },
{ pattern: /t:(\d{4}-\d{2}-\d{2})/g, key: 't:', type: 'absolute' },
function extractSpeakingDates(body: string): Pick<DateAttributes, 'due'|'t'> {
const expressions: { pattern: RegExp, key: 'due'|'t', type: string }[] = [
{ pattern: /due:(?!(\d{4}-\d{2}-\d{2}))(.*?)(?=t:|$)/g, key: 'due', type: 'relative' },
{ pattern: /due:(\d{4}-\d{2}-\d{2})/g, key: 'due', type: 'absolute' },
{ pattern: /t:(?!(\d{4}-\d{2}-\d{2}))(.*?)(?=due:|$)/g, key: 't', type: 'relative' },
{ pattern: /t:(\d{4}-\d{2}-\d{2})/g, key: 't', type: 'absolute' },
];

const speakingDates: DateAttributes = {
'due:': {
const speakingDates: Pick<DateAttributes, 'due'|'t'> = {
'due': {
date: null,
string: null,
type: null,
notify: false,
},
't:': {
't': {
date: null,
string: null,
type: null,
Expand All @@ -86,4 +86,54 @@ function extractSpeakingDates(body: string): DateAttributes {
return speakingDates;
}

export { extractSpeakingDates, replaceSpeakingDatesWithAbsoluteDates };
function friendlyDateGroup(date: Dayjs): FriendlyDateGroup | null {
const today = dayjs();

if (!date || !date.isValid()) {
return null;
}

if (date.isBefore(today.subtract(1, 'week'))) {
return 'before-last-week';
}

if (date.isBefore(today.subtract(1, 'day'))) {
return 'last-week';
}

if (date.isBefore(today)) {
return 'yesterday';
}

if (date.isSame(today)) {
return 'today';
}

if (date.isSame(today.add(1, 'day'))) {
return 'tomorrow';
}

if (date.isBefore(today.add(1, 'week').add(1, 'day'))) {
return 'this-week';
}

if (date.isBefore(today.add(2, 'week').add(1, 'day'))) {
return 'next-week';
}

if (date.isBefore(today.add(1, 'month').add(1, 'day'))) {
return 'this-month';
}

if (date.isBefore(today.add(2, 'month').add(1, 'day'))) {
return 'next-month';
}

return 'after-next-month';
}

export {
extractSpeakingDates,
friendlyDateGroup,
replaceSpeakingDatesWithAbsoluteDates
};
10 changes: 6 additions & 4 deletions src/renderer/Grid/Group.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ import Divider from '@mui/material/Divider';
import dayjs from 'dayjs';
import updateLocale from 'dayjs/plugin/updateLocale';
import { friendlyDate } from 'renderer/Shared';
import { i18n } from 'renderer/Settings/LanguageSelector';
import { WithTranslation, withTranslation } from 'react-i18next';
import type { i18n } from 'renderer/Settings/LanguageSelector';
import { type WithTranslation, withTranslation } from 'react-i18next';
import { getDateAttributes } from 'main/modules/Attributes';
dayjs.extend(updateLocale);

interface FormatGroupElementProps {
Expand All @@ -18,12 +19,13 @@ interface FormatGroupElementProps {

function formatGroupElement({ groupElement, settings, t, todotxtAttribute }: FormatGroupElementProps) {
// If group element is a date, then format according to user preferences
const dateAttributeKeys = Object.keys(getDateAttributes());
if (
['due', 't'].includes(todotxtAttribute)
dateAttributeKeys.includes(todotxtAttribute)
&& dayjs(groupElement).isValid()
&& settings.useHumanFriendlyDates
) {
return friendlyDate(groupElement, todotxtAttribute, settings, t).pop();
return friendlyDate(groupElement, todotxtAttribute, t).pop();
}

// No transformation required: display as-is
Expand Down
78 changes: 39 additions & 39 deletions src/renderer/Shared.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import calendar from 'dayjs/plugin/calendar';
import weekday from 'dayjs/plugin/weekday';
import updateLocale from 'dayjs/plugin/updateLocale';
import { i18n } from './Settings/LanguageSelector';
import { friendlyDateGroup } from 'main/modules/Date';
dayjs.extend(relativeTime);
dayjs.extend(duration);
dayjs.extend(calendar);
Expand Down Expand Up @@ -68,48 +69,47 @@ export const friendlyDate = (value: string, attributeKey: string, settings: Sett
weekStart: settings.weekStart,
});

const today = dayjs();
const date = dayjs(value);
const results = [];

if (date.isBefore(today, 'day')) {
results.push((attributeKey === 'due') ? t('drawer.attributes.overdue') : t('drawer.attributes.elapsed'));
}

if (date.isAfter(today.subtract(1, 'week').startOf('week').subtract(1, 'day')) && date.isBefore(today.subtract(1, 'week').endOf('week'))) {
results.push(t('drawer.attributes.lastWeek'));
}

if (date.isBefore(today.endOf('month')) && date.isAfter(today.subtract(1, 'day'), 'day')) {
results.push(t('drawer.attributes.thisMonth'));
}

if (date.isSame(today, 'week')) {
results.push(t('drawer.attributes.thisWeek'));
}

if (date.isSame(today.subtract(1, 'day'), 'day')) {
results.push(t('drawer.attributes.yesterday'));
}
const group = friendlyDateGroup(date);

if (date.isSame(today, 'day')) {
results.push(t('drawer.attributes.today'));
}

if (date.isSame(today.add(1, 'day'), 'day')) {
results.push(t('drawer.attributes.tomorrow'));
}

if (date.isSame(today.add(1, 'week'), 'week')) {
results.push(t('drawer.attributes.nextWeek'));
}

if (date.month() === today.add(1, 'month').month()) {
results.push(t('drawer.attributes.nextMonth'));
}
const results = [];

if (date.isAfter(today.add(1, 'month').endOf('month'))) {
results.push(dayjs(date).format('YYYY-MM-DD'));
switch (group) {
case null:
// This should never happen, as `date` will always be a valid date
break;
case 'before-last-week':
results.push((attributeKey === 'due') ? t('drawer.attributes.overdue') : t('drawer.attributes.elapsed'));
break;
case 'last-week':
results.push((attributeKey === 'due') ? t('drawer.attributes.overdue') : t('drawer.attributes.elapsed'));
results.push(t('drawer.attributes.lastWeek'));
break;
case 'yesterday':
results.push((attributeKey === 'due') ? t('drawer.attributes.overdue') : t('drawer.attributes.elapsed'));
results.push(t('drawer.attributes.yesterday'));
break;
case 'today':
results.push(t('drawer.attributes.today'));
break;
case 'tomorrow':
results.push(t('drawer.attributes.tomorrow'));
break;
case 'this-week':
results.push(t('drawer.attributes.thisWeek'));
break;
case 'next-week':
results.push(t('drawer.attributes.nextWeek'));
break;
case 'this-month':
results.push(t('drawer.attributes.thisMonth'));
break;
case 'next-month':
results.push(t('drawer.attributes.nextMonth'));
break;
case 'after-next-month':
results.push(dayjs(date).format('YYYY-MM-DD'));
break;
}

return results;
Expand Down
Loading

0 comments on commit c3fceea

Please sign in to comment.