mirror of
https://github.com/immich-app/immich.git
synced 2025-07-31 15:08:44 -04:00
fix(web): timeline time bucket issue (#20438)
This commit is contained in:
parent
097e132fba
commit
d5a01c0310
@ -29,12 +29,7 @@
|
|||||||
import { deleteAssets, updateStackedAssetInTimeline, updateUnstackedAssetInTimeline } from '$lib/utils/actions';
|
import { deleteAssets, updateStackedAssetInTimeline, updateUnstackedAssetInTimeline } from '$lib/utils/actions';
|
||||||
import { archiveAssets, cancelMultiselect, selectAllAssets, stackAssets } from '$lib/utils/asset-utils';
|
import { archiveAssets, cancelMultiselect, selectAllAssets, stackAssets } from '$lib/utils/asset-utils';
|
||||||
import { navigate } from '$lib/utils/navigation';
|
import { navigate } from '$lib/utils/navigation';
|
||||||
import {
|
import { getTimes, toTimelineAsset, type ScrubberListener, type TimelineYearMonth } from '$lib/utils/timeline-util';
|
||||||
getTimes,
|
|
||||||
toTimelineAsset,
|
|
||||||
type ScrubberListener,
|
|
||||||
type TimelinePlainYearMonth,
|
|
||||||
} from '$lib/utils/timeline-util';
|
|
||||||
import { AssetVisibility, getAssetInfo, type AlbumResponseDto, type PersonResponseDto } from '@immich/sdk';
|
import { AssetVisibility, getAssetInfo, type AlbumResponseDto, type PersonResponseDto } from '@immich/sdk';
|
||||||
import { modalManager } from '@immich/ui';
|
import { modalManager } from '@immich/ui';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
@ -343,7 +338,7 @@
|
|||||||
|
|
||||||
const monthsLength = timelineManager.months.length;
|
const monthsLength = timelineManager.months.length;
|
||||||
for (let i = -1; i < monthsLength + 1; i++) {
|
for (let i = -1; i < monthsLength + 1; i++) {
|
||||||
let monthGroup: TimelinePlainYearMonth | undefined;
|
let monthGroup: TimelineYearMonth | undefined;
|
||||||
let monthGroupHeight = 0;
|
let monthGroupHeight = 0;
|
||||||
if (i === -1) {
|
if (i === -1) {
|
||||||
// lead-in
|
// lead-in
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { setDifference, type TimelinePlainDate } from '$lib/utils/timeline-util';
|
import { setDifference, type TimelineDate } from '$lib/utils/timeline-util';
|
||||||
import { AssetOrder } from '@immich/sdk';
|
import { AssetOrder } from '@immich/sdk';
|
||||||
import { SvelteSet } from 'svelte/reactivity';
|
import { SvelteSet } from 'svelte/reactivity';
|
||||||
import type { DayGroup } from './day-group.svelte';
|
import type { DayGroup } from './day-group.svelte';
|
||||||
@ -13,11 +13,11 @@ export class GroupInsertionCache {
|
|||||||
changedDayGroups = new SvelteSet<DayGroup>();
|
changedDayGroups = new SvelteSet<DayGroup>();
|
||||||
newDayGroups = new SvelteSet<DayGroup>();
|
newDayGroups = new SvelteSet<DayGroup>();
|
||||||
|
|
||||||
getDayGroup({ year, month, day }: TimelinePlainDate): DayGroup | undefined {
|
getDayGroup({ year, month, day }: TimelineDate): DayGroup | undefined {
|
||||||
return this.#lookupCache[year]?.[month]?.[day];
|
return this.#lookupCache[year]?.[month]?.[day];
|
||||||
}
|
}
|
||||||
|
|
||||||
setDayGroup(dayGroup: DayGroup, { year, month, day }: TimelinePlainDate) {
|
setDayGroup(dayGroup: DayGroup, { year, month, day }: TimelineDate) {
|
||||||
if (!this.#lookupCache[year]) {
|
if (!this.#lookupCache[year]) {
|
||||||
this.#lookupCache[year] = {};
|
this.#lookupCache[year] = {};
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||||
import { toISOYearMonthUTC } from '$lib/utils/timeline-util';
|
import { toISOYearMonthUTC } from '$lib/utils/timeline-util';
|
||||||
import { getTimeBucket } from '@immich/sdk';
|
import { getTimeBucket } from '@immich/sdk';
|
||||||
|
|
||||||
import type { MonthGroup } from '../month-group.svelte';
|
import type { MonthGroup } from '../month-group.svelte';
|
||||||
import type { TimelineManager } from '../timeline-manager.svelte';
|
import type { TimelineManager } from '../timeline-manager.svelte';
|
||||||
import type { TimelineManagerOptions } from '../types';
|
import type { TimelineManagerOptions } from '../types';
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { setDifference, type TimelinePlainDate } from '$lib/utils/timeline-util';
|
import { setDifference, type TimelineDate } from '$lib/utils/timeline-util';
|
||||||
import { AssetOrder } from '@immich/sdk';
|
import { AssetOrder } from '@immich/sdk';
|
||||||
|
|
||||||
import { SvelteSet } from 'svelte/reactivity';
|
import { SvelteSet } from 'svelte/reactivity';
|
||||||
@ -70,7 +70,7 @@ export function runAssetOperation(
|
|||||||
const changedMonthGroups = new SvelteSet<MonthGroup>();
|
const changedMonthGroups = new SvelteSet<MonthGroup>();
|
||||||
let idsToProcess = new SvelteSet(ids);
|
let idsToProcess = new SvelteSet(ids);
|
||||||
const idsProcessed = new SvelteSet<string>();
|
const idsProcessed = new SvelteSet<string>();
|
||||||
const combinedMoveAssets: { asset: TimelineAsset; date: TimelinePlainDate }[][] = [];
|
const combinedMoveAssets: { asset: TimelineAsset; date: TimelineDate }[][] = [];
|
||||||
for (const month of timelineManager.months) {
|
for (const month of timelineManager.months) {
|
||||||
if (idsToProcess.size > 0) {
|
if (idsToProcess.size > 0) {
|
||||||
const { moveAssets, processedIds, changedGeometry } = month.runAssetOperation(idsToProcess, operation);
|
const { moveAssets, processedIds, changedGeometry } = month.runAssetOperation(idsToProcess, operation);
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { plainDateTimeCompare, type TimelinePlainYearMonth } from '$lib/utils/timeline-util';
|
import { plainDateTimeCompare, type TimelineYearMonth } from '$lib/utils/timeline-util';
|
||||||
import { AssetOrder } from '@immich/sdk';
|
import { AssetOrder } from '@immich/sdk';
|
||||||
import type { MonthGroup } from '../month-group.svelte';
|
import type { MonthGroup } from '../month-group.svelte';
|
||||||
import type { TimelineManager } from '../timeline-manager.svelte';
|
import type { TimelineManager } from '../timeline-manager.svelte';
|
||||||
@ -42,7 +42,7 @@ export function findMonthGroupForAsset(timelineManager: TimelineManager, id: str
|
|||||||
|
|
||||||
export function getMonthGroupByDate(
|
export function getMonthGroupByDate(
|
||||||
timelineManager: TimelineManager,
|
timelineManager: TimelineManager,
|
||||||
targetYearMonth: TimelinePlainYearMonth,
|
targetYearMonth: TimelineYearMonth,
|
||||||
): MonthGroup | undefined {
|
): MonthGroup | undefined {
|
||||||
return timelineManager.months.find(
|
return timelineManager.months.find(
|
||||||
(month) => month.yearMonth.year === targetYearMonth.year && month.yearMonth.month === targetYearMonth.month,
|
(month) => month.yearMonth.year === targetYearMonth.year && month.yearMonth.month === targetYearMonth.month,
|
||||||
@ -135,7 +135,7 @@ export async function retrieveRange(timelineManager: TimelineManager, start: Ass
|
|||||||
return range;
|
return range;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function findMonthGroupForDate(timelineManager: TimelineManager, targetYearMonth: TimelinePlainYearMonth) {
|
export function findMonthGroupForDate(timelineManager: TimelineManager, targetYearMonth: TimelineYearMonth) {
|
||||||
for (const month of timelineManager.months) {
|
for (const month of timelineManager.months) {
|
||||||
const { year, month: monthNum } = month.yearMonth;
|
const { year, month: monthNum } = month.yearMonth;
|
||||||
if (monthNum === targetYearMonth.month && year === targetYearMonth.year) {
|
if (monthNum === targetYearMonth.month && year === targetYearMonth.year) {
|
||||||
|
@ -10,8 +10,8 @@ import {
|
|||||||
fromTimelinePlainYearMonth,
|
fromTimelinePlainYearMonth,
|
||||||
getTimes,
|
getTimes,
|
||||||
setDifference,
|
setDifference,
|
||||||
type TimelinePlainDateTime,
|
type TimelineDateTime,
|
||||||
type TimelinePlainYearMonth,
|
type TimelineYearMonth,
|
||||||
} from '$lib/utils/timeline-util';
|
} from '$lib/utils/timeline-util';
|
||||||
|
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
@ -47,11 +47,11 @@ export class MonthGroup {
|
|||||||
isHeightActual: boolean = $state(false);
|
isHeightActual: boolean = $state(false);
|
||||||
|
|
||||||
readonly monthGroupTitle: string;
|
readonly monthGroupTitle: string;
|
||||||
readonly yearMonth: TimelinePlainYearMonth;
|
readonly yearMonth: TimelineYearMonth;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
store: TimelineManager,
|
store: TimelineManager,
|
||||||
yearMonth: TimelinePlainYearMonth,
|
yearMonth: TimelineYearMonth,
|
||||||
initialCount: number,
|
initialCount: number,
|
||||||
order: AssetOrder = AssetOrder.Desc,
|
order: AssetOrder = AssetOrder.Desc,
|
||||||
) {
|
) {
|
||||||
@ -351,7 +351,7 @@ export class MonthGroup {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
findClosest(target: TimelinePlainDateTime) {
|
findClosest(target: TimelineDateTime) {
|
||||||
const targetDate = fromTimelinePlainDateTime(target);
|
const targetDate = fromTimelinePlainDateTime(target);
|
||||||
let closest = undefined;
|
let closest = undefined;
|
||||||
let smallestDiff = Infinity;
|
let smallestDiff = Infinity;
|
||||||
|
@ -3,7 +3,7 @@ import { AssetOrder, getAssetInfo, getTimeBuckets } from '@immich/sdk';
|
|||||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||||
|
|
||||||
import { CancellableTask } from '$lib/utils/cancellable-task';
|
import { CancellableTask } from '$lib/utils/cancellable-task';
|
||||||
import { toTimelineAsset, type TimelinePlainDateTime, type TimelinePlainYearMonth } from '$lib/utils/timeline-util';
|
import { toTimelineAsset, type TimelineDateTime, type TimelineYearMonth } from '$lib/utils/timeline-util';
|
||||||
|
|
||||||
import { clamp, debounce, isEqual } from 'lodash-es';
|
import { clamp, debounce, isEqual } from 'lodash-es';
|
||||||
import { SvelteDate, SvelteMap, SvelteSet } from 'svelte/reactivity';
|
import { SvelteDate, SvelteMap, SvelteSet } from 'svelte/reactivity';
|
||||||
@ -387,7 +387,7 @@ export class TimelineManager {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadMonthGroup(yearMonth: TimelinePlainYearMonth, options?: { cancelable: boolean }): Promise<void> {
|
async loadMonthGroup(yearMonth: TimelineYearMonth, options?: { cancelable: boolean }): Promise<void> {
|
||||||
let cancelable = true;
|
let cancelable = true;
|
||||||
if (options) {
|
if (options) {
|
||||||
cancelable = options.cancelable;
|
cancelable = options.cancelable;
|
||||||
@ -433,7 +433,7 @@ export class TimelineManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async #loadMonthGroupAtTime(yearMonth: TimelinePlainYearMonth, options?: { cancelable: boolean }) {
|
async #loadMonthGroupAtTime(yearMonth: TimelineYearMonth, options?: { cancelable: boolean }) {
|
||||||
await this.loadMonthGroup(yearMonth, options);
|
await this.loadMonthGroup(yearMonth, options);
|
||||||
return getMonthGroupByDate(this, yearMonth);
|
return getMonthGroupByDate(this, yearMonth);
|
||||||
}
|
}
|
||||||
@ -514,7 +514,7 @@ export class TimelineManager {
|
|||||||
return await getAssetWithOffset(this, assetDescriptor, interval, 'earlier');
|
return await getAssetWithOffset(this, assetDescriptor, interval, 'earlier');
|
||||||
}
|
}
|
||||||
|
|
||||||
async getClosestAssetToDate(dateTime: TimelinePlainDateTime) {
|
async getClosestAssetToDate(dateTime: TimelineDateTime) {
|
||||||
const monthGroup = findMonthGroupForDate(this, dateTime);
|
const monthGroup = findMonthGroupForDate(this, dateTime);
|
||||||
if (!monthGroup) {
|
if (!monthGroup) {
|
||||||
return;
|
return;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import type { TimelinePlainDate, TimelinePlainDateTime } from '$lib/utils/timeline-util';
|
import type { TimelineDate, TimelineDateTime } from '$lib/utils/timeline-util';
|
||||||
import type { AssetStackResponseDto, AssetVisibility } from '@immich/sdk';
|
import type { AssetStackResponseDto, AssetVisibility } from '@immich/sdk';
|
||||||
|
|
||||||
export type AssetApiGetTimeBucketsRequest = Parameters<typeof import('@immich/sdk').getTimeBuckets>[0];
|
export type AssetApiGetTimeBucketsRequest = Parameters<typeof import('@immich/sdk').getTimeBuckets>[0];
|
||||||
@ -17,8 +17,8 @@ export type TimelineAsset = {
|
|||||||
ownerId: string;
|
ownerId: string;
|
||||||
ratio: number;
|
ratio: number;
|
||||||
thumbhash: string | null;
|
thumbhash: string | null;
|
||||||
localDateTime: TimelinePlainDateTime;
|
localDateTime: TimelineDateTime;
|
||||||
fileCreatedAt: TimelinePlainDateTime;
|
fileCreatedAt: TimelineDateTime;
|
||||||
visibility: AssetVisibility;
|
visibility: AssetVisibility;
|
||||||
isFavorite: boolean;
|
isFavorite: boolean;
|
||||||
isTrashed: boolean;
|
isTrashed: boolean;
|
||||||
@ -35,7 +35,7 @@ export type TimelineAsset = {
|
|||||||
|
|
||||||
export type AssetOperation = (asset: TimelineAsset) => { remove: boolean };
|
export type AssetOperation = (asset: TimelineAsset) => { remove: boolean };
|
||||||
|
|
||||||
export type MoveAsset = { asset: TimelineAsset; date: TimelinePlainDate };
|
export type MoveAsset = { asset: TimelineAsset; date: TimelineDate };
|
||||||
|
|
||||||
export interface Viewport {
|
export interface Viewport {
|
||||||
width: number;
|
width: number;
|
||||||
|
@ -7,16 +7,16 @@ import { SvelteSet } from 'svelte/reactivity';
|
|||||||
import { get } from 'svelte/store';
|
import { get } from 'svelte/store';
|
||||||
|
|
||||||
// Move type definitions to the top
|
// Move type definitions to the top
|
||||||
export type TimelinePlainYearMonth = {
|
export type TimelineYearMonth = {
|
||||||
year: number;
|
year: number;
|
||||||
month: number;
|
month: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TimelinePlainDate = TimelinePlainYearMonth & {
|
export type TimelineDate = TimelineYearMonth & {
|
||||||
day: number;
|
day: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TimelinePlainDateTime = TimelinePlainDate & {
|
export type TimelineDateTime = TimelineDate & {
|
||||||
hour: number;
|
hour: number;
|
||||||
minute: number;
|
minute: number;
|
||||||
second: number;
|
second: number;
|
||||||
@ -33,29 +33,26 @@ export type ScrubberListener = (
|
|||||||
export const fromISODateTime = (isoDateTime: string, timeZone: string): DateTime<true> =>
|
export const fromISODateTime = (isoDateTime: string, timeZone: string): DateTime<true> =>
|
||||||
DateTime.fromISO(isoDateTime, { zone: timeZone, locale: get(locale) }) as DateTime<true>;
|
DateTime.fromISO(isoDateTime, { zone: timeZone, locale: get(locale) }) as DateTime<true>;
|
||||||
|
|
||||||
export const fromISODateTimeToObject = (isoDateTime: string, timeZone: string): TimelinePlainDateTime =>
|
export const fromISODateTimeToObject = (isoDateTime: string, timeZone: string): TimelineDateTime =>
|
||||||
(fromISODateTime(isoDateTime, timeZone) as DateTime<true>).toObject();
|
(fromISODateTime(isoDateTime, timeZone) as DateTime<true>).toObject();
|
||||||
|
|
||||||
// used for AssetResponseDto.localDateTime, amongst others
|
// used for AssetResponseDto.localDateTime, amongst others
|
||||||
export const fromISODateTimeUTC = (isoDateTimeUtc: string) => fromISODateTime(isoDateTimeUtc, 'UTC');
|
export const fromISODateTimeUTC = (isoDateTimeUtc: string) => fromISODateTime(isoDateTimeUtc, 'UTC');
|
||||||
|
|
||||||
export const fromISODateTimeUTCToObject = (isoDateTimeUtc: string): TimelinePlainDateTime =>
|
export const fromISODateTimeUTCToObject = (isoDateTimeUtc: string): TimelineDateTime =>
|
||||||
(fromISODateTimeUTC(isoDateTimeUtc) as DateTime<true>).toObject();
|
(fromISODateTimeUTC(isoDateTimeUtc) as DateTime<true>).toObject();
|
||||||
|
|
||||||
// used to create equivalent of AssetResponseDto.localDateTime in UTC, but without timezone information
|
// used to create equivalent of AssetResponseDto.localDateTime in UTC, but without timezone information
|
||||||
export const fromISODateTimeTruncateTZToObject = (
|
export const fromISODateTimeTruncateTZToObject = (
|
||||||
isoDateTimeUtc: string,
|
isoDateTimeUtc: string,
|
||||||
timeZone: string | undefined,
|
timeZone: string | undefined,
|
||||||
): TimelinePlainDateTime =>
|
): TimelineDateTime =>
|
||||||
(
|
(
|
||||||
fromISODateTime(isoDateTimeUtc, timeZone ?? 'UTC').setZone('UTC', { keepLocalTime: true }) as DateTime<true>
|
fromISODateTime(isoDateTimeUtc, timeZone ?? 'UTC').setZone('UTC', { keepLocalTime: true }) as DateTime<true>
|
||||||
).toObject();
|
).toObject();
|
||||||
|
|
||||||
// Used to derive a local date time from an ISO string and a UTC offset in hours
|
// Used to derive a local date time from an ISO string and a UTC offset in hours
|
||||||
export const fromISODateTimeWithOffsetToObject = (
|
export const fromISODateTimeWithOffsetToObject = (isoDateTimeUtc: string, utcOffsetHours: number): TimelineDateTime => {
|
||||||
isoDateTimeUtc: string,
|
|
||||||
utcOffsetHours: number,
|
|
||||||
): TimelinePlainDateTime => {
|
|
||||||
const utcDateTime = fromISODateTimeUTC(isoDateTimeUtc);
|
const utcDateTime = fromISODateTimeUTC(isoDateTimeUtc);
|
||||||
|
|
||||||
// Apply the offset to get the local time
|
// Apply the offset to get the local time
|
||||||
@ -82,23 +79,23 @@ export const getTimes = (isoDateTimeUtc: string, localUtcOffsetHours: number) =>
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fromTimelinePlainDateTime = (timelineDateTime: TimelinePlainDateTime): DateTime<true> =>
|
export const fromTimelinePlainDateTime = (timelineDateTime: TimelineDateTime): DateTime<true> =>
|
||||||
DateTime.fromObject(timelineDateTime, { zone: 'local', locale: get(locale) }) as DateTime<true>;
|
DateTime.fromObject(timelineDateTime, { zone: 'local', locale: get(locale) }) as DateTime<true>;
|
||||||
|
|
||||||
export const fromTimelinePlainDate = (timelineYearMonth: TimelinePlainDate): DateTime<true> =>
|
export const fromTimelinePlainDate = (timelineYearMonth: TimelineDate): DateTime<true> =>
|
||||||
DateTime.fromObject(
|
DateTime.fromObject(
|
||||||
{ year: timelineYearMonth.year, month: timelineYearMonth.month, day: timelineYearMonth.day },
|
{ year: timelineYearMonth.year, month: timelineYearMonth.month, day: timelineYearMonth.day },
|
||||||
{ zone: 'local', locale: get(locale) },
|
{ zone: 'local', locale: get(locale) },
|
||||||
) as DateTime<true>;
|
) as DateTime<true>;
|
||||||
|
|
||||||
export const fromTimelinePlainYearMonth = (timelineYearMonth: TimelinePlainYearMonth): DateTime<true> =>
|
export const fromTimelinePlainYearMonth = (timelineYearMonth: TimelineYearMonth): DateTime<true> =>
|
||||||
DateTime.fromObject(
|
DateTime.fromObject(
|
||||||
{ year: timelineYearMonth.year, month: timelineYearMonth.month },
|
{ year: timelineYearMonth.year, month: timelineYearMonth.month },
|
||||||
{ zone: 'local', locale: get(locale) },
|
{ zone: 'local', locale: get(locale) },
|
||||||
) as DateTime<true>;
|
) as DateTime<true>;
|
||||||
|
|
||||||
export const toISOYearMonthUTC = (timelineYearMonth: TimelinePlainYearMonth): string =>
|
export const toISOYearMonthUTC = ({ year, month }: TimelineYearMonth): string =>
|
||||||
(fromTimelinePlainYearMonth(timelineYearMonth).setZone('UTC', { keepLocalTime: true }) as DateTime<true>).toISO();
|
`${year}-${month.toString().padStart(2, '0')}-01T00:00:00.000Z`;
|
||||||
|
|
||||||
export function formatMonthGroupTitle(_date: DateTime): string {
|
export function formatMonthGroupTitle(_date: DateTime): string {
|
||||||
if (!_date.isValid) {
|
if (!_date.isValid) {
|
||||||
@ -193,7 +190,7 @@ export const toTimelineAsset = (unknownAsset: AssetResponseDto | TimelineAsset):
|
|||||||
export const isTimelineAsset = (unknownAsset: AssetResponseDto | TimelineAsset): unknownAsset is TimelineAsset =>
|
export const isTimelineAsset = (unknownAsset: AssetResponseDto | TimelineAsset): unknownAsset is TimelineAsset =>
|
||||||
(unknownAsset as TimelineAsset).ratio !== undefined;
|
(unknownAsset as TimelineAsset).ratio !== undefined;
|
||||||
|
|
||||||
export const plainDateTimeCompare = (ascending: boolean, a: TimelinePlainDateTime, b: TimelinePlainDateTime) => {
|
export const plainDateTimeCompare = (ascending: boolean, a: TimelineDateTime, b: TimelineDateTime) => {
|
||||||
const [aDateTime, bDateTime] = ascending ? [a, b] : [b, a];
|
const [aDateTime, bDateTime] = ascending ? [a, b] : [b, a];
|
||||||
|
|
||||||
if (aDateTime.year !== bDateTime.year) {
|
if (aDateTime.year !== bDateTime.year) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user