Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add Reminder Field Support #2750

Draft
wants to merge 83 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 68 commits
Commits
Show all changes
83 commits
Select commit Hold shift + click to select a range
0dd8b80
not updating tests
stevendkwtz Mar 12, 2024
b1b41ce
Add toggle task endpoint
stevendkwtz Mar 7, 2024
25bfe3a
Make toggleTask return tasks
stevendkwtz Mar 7, 2024
b107bf2
add new api function
stevendkwtz Mar 7, 2024
424c90c
add reminder
stevendkwtz Mar 11, 2024
0ff168b
fix Edit Task
stevendkwtz Mar 11, 2024
767129a
Fix reminders
stevendkwtz Mar 11, 2024
7348320
Fix reminders
stevendkwtz Mar 11, 2024
40cd2e5
updating tests
stevendkwtz Mar 14, 2024
2904b16
remove mapObjToTasks from custom API
stevendkwtz Mar 14, 2024
87db8b3
not updating tests
stevendkwtz Mar 12, 2024
7955956
Merge branch 'main' of github.com:stevendkwtz/obsidian-tasks into main
stevendkwtz Mar 20, 2024
68f6a7c
Add toggle task endpoint
stevendkwtz Mar 7, 2024
8579952
Make toggleTask return tasks
stevendkwtz Mar 7, 2024
1842d84
add new api function
stevendkwtz Mar 7, 2024
22192d0
add reminder
stevendkwtz Mar 11, 2024
478a2f4
Fix reminders
stevendkwtz Mar 11, 2024
7710e90
Fix reminders
stevendkwtz Mar 11, 2024
ec76d68
updating tests
stevendkwtz Mar 14, 2024
15578cc
remove mapObjToTasks from custom API
stevendkwtz Mar 14, 2024
7fafed2
rebase from upstream
stevendkwtz Mar 20, 2024
2ff6393
rebase from upstream
stevendkwtz Mar 20, 2024
1a39f3e
Merge branch 'develop' of github.com:stevendkwtz/obsidian-tasks into …
stevendkwtz Mar 20, 2024
dec9acf
rebase from upstream
stevendkwtz Mar 20, 2024
235b898
not updating tests
stevendkwtz Mar 12, 2024
d72285a
Add toggle task endpoint
stevendkwtz Mar 7, 2024
b3aecd6
Make toggleTask return tasks
stevendkwtz Mar 7, 2024
18c5178
add new api function
stevendkwtz Mar 7, 2024
f22d7c8
add reminder
stevendkwtz Mar 11, 2024
9a9049d
fix Edit Task
stevendkwtz Mar 11, 2024
038f9fc
Fix reminders
stevendkwtz Mar 11, 2024
fa94474
Fix reminders
stevendkwtz Mar 11, 2024
ec23842
updating tests
stevendkwtz Mar 14, 2024
4c43766
remove mapObjToTasks from custom API
stevendkwtz Mar 14, 2024
8f21185
not updating tests
stevendkwtz Mar 12, 2024
7062e3b
Add toggle task endpoint
stevendkwtz Mar 7, 2024
4c9cf2e
Make toggleTask return tasks
stevendkwtz Mar 7, 2024
b1221d3
add new api function
stevendkwtz Mar 7, 2024
7a7b139
add reminder
stevendkwtz Mar 11, 2024
2b17fb0
Fix reminders
stevendkwtz Mar 11, 2024
9cfc974
Fix reminders
stevendkwtz Mar 11, 2024
a8bdbfe
updating tests
stevendkwtz Mar 14, 2024
0f170fb
remove mapObjToTasks from custom API
stevendkwtz Mar 14, 2024
f480d58
rebase from upstream
stevendkwtz Mar 20, 2024
c37aca1
rebase from upstream
stevendkwtz Mar 20, 2024
0b675ab
rebase from upstream
stevendkwtz Mar 20, 2024
7c21f36
Omg im out of rebase hell
stevendkwtz Mar 29, 2024
7ac756a
Omg im out of rebase hell
stevendkwtz Mar 29, 2024
0bba78b
Merge branch 'develop' of github.com:stevendkwtz/obsidian-tasks into …
stevendkwtz Mar 29, 2024
cceec32
more merge hell
stevendkwtz Mar 29, 2024
f858e2f
Fix tests with Clare for reminders
stevendkwtz Apr 2, 2024
01b97a8
Fix failing tests, saving of reminder fields with date time
stevendkwtz Apr 5, 2024
33e3271
chore: Revert changes to lefthook.yml
claremacrae Apr 5, 2024
6eb0648
chore: Revert changes to manifest.json
claremacrae Apr 5, 2024
678cf49
chore: Revert changes to package.json
claremacrae Apr 5, 2024
1b5b1a9
chore: Remove added file package-lock.json
claremacrae Apr 5, 2024
1637a48
refactor: Remove stray spaces in EditTask.svelte
claremacrae Apr 5, 2024
d283759
revert: Remove changes in src/Api/
claremacrae Apr 5, 2024
4d0dd69
chore: Revert changes to yarn.lock
claremacrae Apr 5, 2024
d58efdb
comment: Revert likely accidental change
claremacrae Apr 5, 2024
5b81fba
test: Reinstate an accidentally deleted test
claremacrae Apr 5, 2024
cd1c134
test: Revert changes to what turned out to be a fragile test
claremacrae Apr 5, 2024
02f467d
test: - Add tests for recurrence based on reminder - one is failing
claremacrae Apr 9, 2024
22e5261
test: - Confirm Recurrence honours recurrence time
claremacrae Apr 9, 2024
f4069dd
refactor: Move isDateTime() to DateTools.ts
claremacrae Apr 9, 2024
7690614
test: Add tests for isDateTime()
claremacrae Apr 9, 2024
d5370e6
test: - Extract helper function
claremacrae Apr 9, 2024
26eda75
test: - Reuse helper function
claremacrae Apr 9, 2024
a1f5a17
Merge branch '7.1.0-temp' into fork/stevendkwtz-reminders-merge-7.1.0
claremacrae May 7, 2024
3bc67e2
Merge branch 'main' into fork/stevendkwtz-reminders-merge-7.1.0
claremacrae May 7, 2024
d26f995
test: Check DateTools functions with times. One test marked 'failing'
claremacrae May 7, 2024
8e559b2
test: Check TaskBuilder retains times on Reminder (it doesn't)
claremacrae May 7, 2024
fa22287
test: Confirm that Task parses times on reminders
claremacrae May 7, 2024
9ab7365
test: Check that EditTask preserves time on reminder (1 failing test)
claremacrae May 7, 2024
906d702
test: Add TODO to add a time to the sample reminder value
claremacrae May 7, 2024
1a9b4f5
test: Add formatAsDateAndTimeOrDate() to Task Properties snippet, for…
claremacrae May 7, 2024
cb1aaa3
Update DateTools.parseTypedDateForDisplay to handle date time for rem…
stevendkwtz May 7, 2024
e385b25
Update DateTools.parseTypedDateForDisplay to handle partial reminder …
stevendkwtz May 7, 2024
62f119b
Update DateTools.parseTypedDateForDisplay to handle reminder date par…
stevendkwtz May 7, 2024
bc2f9df
Fix failing EditTask test for reminder date time, add new function to…
stevendkwtz May 21, 2024
0f50f43
test: Add failing test showing reminder does not parse free text
claremacrae May 21, 2024
c93fa6b
refactor: Reformat long line
claremacrae May 21, 2024
791fd53
Merge branch 'main' into fork/stevendkwtz-reminders
claremacrae May 25, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,6 @@ yarn-error.log

# Backup files.
*.bak

# Yarn
.yarn
1 change: 1 addition & 0 deletions .yarnrc.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
nodeLinker: node-modules
2 changes: 2 additions & 0 deletions src/Commands/CreateOrEditTaskParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ export const taskFromLine = ({ line, path }: { line: string; path: string }): Ta
startDate: null,
scheduledDate: null,
dueDate: null,
reminderDate: null,
doneDate: null,
cancelledDate: null,
recurrence: null,
Expand Down Expand Up @@ -128,6 +129,7 @@ export const taskFromLine = ({ line, path }: { line: string; path: string }): Ta
startDate: null,
scheduledDate: null,
dueDate: null,
reminderDate: null,
doneDate: null,
cancelledDate: null,
recurrence: null,
Expand Down
1 change: 1 addition & 0 deletions src/Layout/TaskLayoutOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export enum TaskLayoutComponent {
StartDate = 'startDate',
ScheduledDate = 'scheduledDate',
DueDate = 'dueDate',
ReminderDate = 'reminderDate',
CancelledDate = 'cancelledDate',
DoneDate = 'doneDate',
BlockLink = 'blockLink',
Expand Down
18 changes: 18 additions & 0 deletions src/Query/Filter/ReminderDateField.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type { Moment } from 'moment';
import type { Task } from '../../Task/Task';
import { DateField } from './DateField';

/**
* Support the 'reminder' search instruction.
*/
export class ReminderDateField extends DateField {
public fieldName(): string {
return 'reminder';
}
public date(task: Task): Moment | null {
return task.reminderDate;
}
protected filterResultIfFieldMissing() {
return false;
}
}
2 changes: 2 additions & 0 deletions src/Query/FilterParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { DescriptionField } from './Filter/DescriptionField';
import { CreatedDateField } from './Filter/CreatedDateField';
import { DoneDateField } from './Filter/DoneDateField';
import { DueDateField } from './Filter/DueDateField';
import { ReminderDateField } from './Filter/ReminderDateField';
import { ExcludeSubItemsField } from './Filter/ExcludeSubItemsField';
import { FunctionField } from './Filter/FunctionField';
import { HeadingField } from './Filter/HeadingField';
Expand Down Expand Up @@ -50,6 +51,7 @@ export const fieldCreators: EndsWith<BooleanField> = [
() => new StartDateField(),
() => new ScheduledDateField(),
() => new DueDateField(),
() => new ReminderDateField(),
() => new DoneDateField(),
() => new PathField(),
() => new FolderField(),
Expand Down
5 changes: 4 additions & 1 deletion src/Query/Query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export class Query implements IQuery {
private _ignoreGlobalQuery: boolean = false;

private readonly hideOptionsRegexp =
/^(hide|show) (task count|backlink|priority|cancelled date|created date|start date|scheduled date|done date|due date|recurrence rule|edit button|postpone button|urgency|tags|depends on|id)/i;
/^(hide|show) (task count|backlink|priority|cancelled date|created date|start date|scheduled date|done date|due date|reminder date|recurrence rule|edit button|postpone button|urgency|tags|depends on|id)/i;
private readonly shortModeRegexp = /^short/i;
private readonly fullModeRegexp = /^full/i;
private readonly explainQueryRegexp = /^explain/i;
Expand Down Expand Up @@ -309,6 +309,9 @@ Problem line: "${line}"`;
case 'due date':
this._taskLayoutOptions.setVisibility(TaskLayoutComponent.DueDate, !hide);
break;
case 'reminder date':
this._taskLayoutOptions.setVisibility(TaskLayoutComponent.ReminderDate, !hide);
break;
case 'done date':
this._taskLayoutOptions.setVisibility(TaskLayoutComponent.DoneDate, !hide);
break;
Expand Down
2 changes: 2 additions & 0 deletions src/Query/Sort/Sort.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Task } from '../../Task/Task';
import { StatusTypeField } from '../Filter/StatusTypeField';
import { DueDateField } from '../Filter/DueDateField';
import { ReminderDateField } from '../Filter/ReminderDateField';
import { PriorityField } from '../Filter/PriorityField';
import { PathField } from '../Filter/PathField';
import { UrgencyField } from '../Filter/UrgencyField';
Expand All @@ -27,6 +28,7 @@ export class Sort {
new StatusTypeField().createNormalSorter(),
new UrgencyField().createNormalSorter(),
new DueDateField().createNormalSorter(),
new ReminderDateField().createNormalSorter(),
new PriorityField().createNormalSorter(),
new PathField().createNormalSorter(),
];
Expand Down
1 change: 1 addition & 0 deletions src/Renderer/TaskFieldRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ const taskFieldHTMLData: { [c in TaskLayoutComponent]: TaskFieldHTMLData } = {
// NEW_TASK_FIELD_EDIT_REQUIRED
createdDate: createDateField('task-created', 'taskCreated'),
dueDate: createDateField('task-due', 'taskDue'),
reminderDate: createDateField('task-reminder', 'taskReminder'),
startDate: createDateField('task-start', 'taskStart'),
scheduledDate: createDateField('task-scheduled', 'taskScheduled'),
doneDate: createDateField('task-done', 'taskDone'),
Expand Down
10 changes: 7 additions & 3 deletions src/Renderer/TaskLineRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { StatusRegistry } from '../Statuses/StatusRegistry';
import type { Task } from '../Task/Task';
import { TaskRegularExpressions } from '../Task/TaskRegularExpressions';
import { StatusMenu } from '../ui/Menus/StatusMenu';
import { isDateTime } from '../lib/DateTools';
import { TaskFieldRenderer } from './TaskFieldRenderer';

/**
Expand Down Expand Up @@ -314,6 +315,7 @@ export class TaskLineRenderer {
createdDateSymbol,
scheduledDateSymbol,
dueDateSymbol,
reminderDateSymbol,
cancelledDateSymbol,
doneDateSymbol,
} = TASK_FORMATS.tasksPluginEmoji.taskSerializer.symbols;
Expand All @@ -332,9 +334,10 @@ export class TaskLineRenderer {
}

function toTooltipDate({ signifier, date }: { signifier: string; date: Moment }): string {
return `${signifier} ${date.format(TaskRegularExpressions.dateFormat)} (${date.from(
window.moment().startOf('day'),
)})`;
const format = isDateTime(date)
? TaskRegularExpressions.dateTimeFormat
: TaskRegularExpressions.dateFormat;
return `${signifier} ${date.format(format)} (${date.from(window.moment().startOf('day'))})`;
}

const tooltip = element.createDiv();
Expand All @@ -350,6 +353,7 @@ export class TaskLineRenderer {
addDateToTooltip(tooltip, task.startDate, startDateSymbol);
addDateToTooltip(tooltip, task.scheduledDate, scheduledDateSymbol);
addDateToTooltip(tooltip, task.dueDate, dueDateSymbol);
addDateToTooltip(tooltip, task.reminderDate, reminderDateSymbol);
addDateToTooltip(tooltip, task.cancelledDate, cancelledDateSymbol);
addDateToTooltip(tooltip, task.doneDate, doneDateSymbol);

Expand Down
9 changes: 9 additions & 0 deletions src/Scripting/TasksDate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { DurationInputArg2, Moment, unitOfTime } from 'moment';
import { Notice } from 'obsidian';
import { PropertyCategory } from '../lib/PropertyCategory';
import { TaskRegularExpressions } from '../Task/TaskRegularExpressions';
import { isDateTime } from '../lib/DateTools';

/**
* TasksDate encapsulates a date, for simplifying the JavaScript expressions users need to
Expand Down Expand Up @@ -37,6 +38,14 @@ export class TasksDate {
return this.format(TaskRegularExpressions.dateTimeFormat, fallBackText);
}

/**
* Return the date formatted as YYYY-MM-DD HH:mm, or {@link fallBackText} if there is no date.
@param fallBackText - the string to use if the date is null. Defaults to empty string.
*/
public formatAsDateAndTimeOrDate(fallBackText: string = ''): string {
return isDateTime(this.moment) ? this.formatAsDateAndTime(fallBackText) : this.formatAsDate(fallBackText);
}

/**
* Return the date formatted with the given format string, or {@link fallBackText} if there is no date.
* See https://momentjs.com/docs/#/displaying/ for all the available formatting options.
Expand Down
13 changes: 12 additions & 1 deletion src/Suggestor/Suggestor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,12 @@ export function makeDefaultSuggestionBuilder(
dataviewMode: boolean,
): SuggestionBuilder {
// NEW_TASK_FIELD_EDIT_REQUIRED
const datePrefixRegex = [symbols.startDateSymbol, symbols.scheduledDateSymbol, symbols.dueDateSymbol].join('|');
const datePrefixRegex = [
symbols.startDateSymbol,
symbols.scheduledDateSymbol,
symbols.dueDateSymbol,
symbols.reminderDateSymbol,
].join('|');
/*
* Return a list of suggestions, either generic or more fine-grained to the words at the cursor.
*/
Expand Down Expand Up @@ -97,6 +102,11 @@ function addTaskPropertySuggestions(
displayText: `${symbols.dueDateSymbol} due date`,
appendText: `${symbols.dueDateSymbol} `,
});
if (!line.includes(symbols.reminderDateSymbol))
genericSuggestions.push({
displayText: `${symbols.reminderDateSymbol} reminder date`,
appendText: `${symbols.reminderDateSymbol} `,
});
if (!line.includes(symbols.startDateSymbol))
genericSuggestions.push({
displayText: `${symbols.startDateSymbol} start date`,
Expand Down Expand Up @@ -320,6 +330,7 @@ function addRecurrenceSuggestions(
startDate: null,
scheduledDate: null,
dueDate: null,
reminderDate: null,
})?.toText();
if (parsedRecurrence) {
const appendedText = `${recurrencePrefix} ${parsedRecurrence}` + postfix;
Expand Down
23 changes: 23 additions & 0 deletions src/Task/Recurrence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export class Recurrence {
private readonly startDate: Moment | null;
private readonly scheduledDate: Moment | null;
private readonly dueDate: Moment | null;
private readonly reminderDate: Moment | null;

/**
* The reference date is used to calculate future occurrences.
Expand All @@ -34,32 +35,37 @@ export class Recurrence {
startDate,
scheduledDate,
dueDate,
reminderDate,
}: {
rrule: RRule;
baseOnToday: boolean;
referenceDate: Moment | null;
startDate: Moment | null;
scheduledDate: Moment | null;
dueDate: Moment | null;
reminderDate: Moment | null;
}) {
this.rrule = rrule;
this.baseOnToday = baseOnToday;
this.referenceDate = referenceDate;
this.startDate = startDate;
this.scheduledDate = scheduledDate;
this.dueDate = dueDate;
this.reminderDate = reminderDate;
}

public static fromText({
recurrenceRuleText,
startDate,
scheduledDate,
dueDate,
reminderDate,
}: {
recurrenceRuleText: string;
startDate: Moment | null;
scheduledDate: Moment | null;
dueDate: Moment | null;
reminderDate: Moment | null;
}): Recurrence | null {
try {
const match = recurrenceRuleText.match(/^([a-zA-Z0-9, !]+?)( when done)?$/i);
Expand All @@ -78,6 +84,8 @@ export class Recurrence {
// Clone the moment objects.
if (dueDate) {
referenceDate = window.moment(dueDate);
} else if (reminderDate) {
referenceDate = window.moment(reminderDate);
} else if (scheduledDate) {
referenceDate = window.moment(scheduledDate);
} else if (startDate) {
Expand All @@ -98,6 +106,7 @@ export class Recurrence {
startDate,
scheduledDate,
dueDate,
reminderDate,
});
}
} catch (e) {
Expand Down Expand Up @@ -129,6 +138,7 @@ export class Recurrence {
startDate: Moment | null;
scheduledDate: Moment | null;
dueDate: Moment | null;
reminderDate: Moment | null;
} | null {
const next = this.nextReferenceDate(today);

Expand All @@ -138,6 +148,7 @@ export class Recurrence {
let startDate: Moment | null = null;
let scheduledDate: Moment | null = null;
let dueDate: Moment | null = null;
let reminderDate: Moment | null = null;

// Only if a reference date is given. A reference date will exist if at
// least one of the other dates is set.
Expand Down Expand Up @@ -166,12 +177,21 @@ export class Recurrence {
// Rounding days to handle cross daylight-savings-time recurrences.
dueDate.add(Math.round(originalDifference.asDays()), 'days');
}
if (this.reminderDate) {
const originalDifference = window.moment.duration(this.reminderDate.diff(this.referenceDate));

// Cloning so that original won't be manipulated:
reminderDate = window.moment(next);
// Rounding days to handle cross daylight-savings-time recurrences.
reminderDate.add(Math.round(originalDifference.asDays()), 'days');
}
}

return {
startDate,
scheduledDate,
dueDate,
reminderDate,
};
}

Expand All @@ -193,6 +213,9 @@ export class Recurrence {
if (compareByDate(this.dueDate, other.dueDate) !== 0) {
return false;
}
if (compareByDate(this.reminderDate, other.reminderDate) !== 0) {
return false;
}

return this.toText() === other.toText(); // this also checks baseOnToday
}
Expand Down
14 changes: 14 additions & 0 deletions src/Task/Task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export class Task {
public readonly startDate: Moment | null;
public readonly scheduledDate: Moment | null;
public readonly dueDate: Moment | null;
public readonly reminderDate: Moment | null;
public readonly doneDate: Moment | null;
public readonly cancelledDate: Moment | null;

Expand Down Expand Up @@ -88,6 +89,7 @@ export class Task {
startDate,
scheduledDate,
dueDate,
reminderDate,
doneDate,
cancelledDate,
recurrence,
Expand All @@ -109,6 +111,7 @@ export class Task {
startDate: moment.Moment | null;
scheduledDate: moment.Moment | null;
dueDate: moment.Moment | null;
reminderDate: moment.Moment | null;
doneDate: moment.Moment | null;
cancelledDate: moment.Moment | null;
recurrence: Recurrence | null;
Expand All @@ -134,6 +137,7 @@ export class Task {
this.startDate = startDate;
this.scheduledDate = scheduledDate;
this.dueDate = dueDate;
this.reminderDate = reminderDate;
this.doneDate = doneDate;
this.cancelledDate = cancelledDate;

Expand Down Expand Up @@ -362,6 +366,7 @@ export class Task {
startDate: Moment | null;
scheduledDate: Moment | null;
dueDate: Moment | null;
reminderDate: Moment | null;
} | null = null;
if (newStatus.isCompleted()) {
if (!this.status.isCompleted() && this.recurrence !== null) {
Expand Down Expand Up @@ -423,6 +428,7 @@ export class Task {
startDate: moment.Moment | null;
scheduledDate: moment.Moment | null;
dueDate: moment.Moment | null;
reminderDate: moment.Moment | null;
},
) {
const { setCreatedDate } = getSettings();
Expand Down Expand Up @@ -649,6 +655,13 @@ export class Task {
return new TasksDate(this.dueDate);
}

/**
* Return {@link reminderDate} as a {@link TasksDate}, so the field names in scripting docs are consistent with the existing search instruction names, and null values are easy to deal with.
*/
public get reminder(): TasksDate {
return new TasksDate(this.reminderDate);
}

/**
* Return {@link scheduledDate} as a {@link TasksDate}, so the field names in scripting docs are consistent with the existing search instruction names, and null values are easy to deal with.
*/
Expand Down Expand Up @@ -893,6 +906,7 @@ export class Task {
'startDate' as keyof Task,
'scheduledDate' as keyof Task,
'dueDate' as keyof Task,
'reminderDate' as keyof Task,
'doneDate' as keyof Task,
'cancelledDate' as keyof Task,
];
Expand Down
Loading