mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 21:15:32 +09:00
fix: various fixes to things + simply app proxy sentry setup
This commit is contained in:
44 files changed
+441
-1042
No files matched your search
@@ -310,10 +310,6 @@ class AccountStorage {
|
||||
sso: instance.sso,
|
||||
publicPushVapidKey: instance.publicPushVapidKey,
|
||||
sentryDsn: instance.sentryDsn ?? '',
|
||||
sentryProxyPath: instance.sentryProxyPath ?? '',
|
||||
sentryReportHost: instance.sentryReportHost ?? '',
|
||||
sentryProjectId: instance.sentryProjectId ?? '',
|
||||
sentryPublicKey: instance.sentryPublicKey ?? '',
|
||||
limits:
|
||||
instance.limits !== undefined && instance.limits !== null
|
||||
? JSON.parse(JSON.stringify(instance.limits))
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {
|
||||
MarkdownContext,
|
||||
type MarkdownRenderOptions,
|
||||
type RendererProps,
|
||||
} from '@app/lib/markdown/renderers/RendererTypes';
|
||||
import {TimestampRenderer} from '@app/lib/markdown/renderers/TimestampRenderer';
|
||||
import {NodeType, TimestampStyle} from '@fluxer/markdown_parser/src/types/Enums';
|
||||
import type {TimestampNode} from '@fluxer/markdown_parser/src/types/Nodes';
|
||||
import {setupI18n} from '@lingui/core';
|
||||
import React from 'react';
|
||||
import {renderToStaticMarkup} from 'react-dom/server';
|
||||
import {describe, expect, test} from 'vitest';
|
||||
|
||||
const i18n = setupI18n({locale: 'en-US', messages: {'en-US': {}}});
|
||||
|
||||
function createRendererProps(node: TimestampNode): RendererProps<TimestampNode> {
|
||||
const options: MarkdownRenderOptions = {
|
||||
context: MarkdownContext.STANDARD_WITHOUT_JUMBO,
|
||||
shouldJumboEmojis: false,
|
||||
i18n,
|
||||
};
|
||||
|
||||
return {
|
||||
node,
|
||||
id: 'test-timestamp',
|
||||
renderChildren: () => null,
|
||||
options,
|
||||
};
|
||||
}
|
||||
|
||||
describe('TimestampRenderer', () => {
|
||||
test('renders a non-throwing fallback for invalid timestamps', () => {
|
||||
const props = createRendererProps({
|
||||
type: NodeType.Timestamp,
|
||||
timestamp: Number.POSITIVE_INFINITY,
|
||||
style: TimestampStyle.ShortDateTime,
|
||||
});
|
||||
|
||||
const renderFn = () => renderToStaticMarkup(React.createElement(TimestampRenderer, props));
|
||||
|
||||
expect(renderFn).not.toThrow();
|
||||
const markup = renderFn();
|
||||
expect(markup).toContain('Infinity');
|
||||
expect(markup).not.toContain('<time');
|
||||
});
|
||||
|
||||
test('renders a semantic time element for valid timestamps', () => {
|
||||
const props = createRendererProps({
|
||||
type: NodeType.Timestamp,
|
||||
timestamp: 1618953630,
|
||||
style: TimestampStyle.ShortDateTime,
|
||||
});
|
||||
|
||||
const markup = renderToStaticMarkup(React.createElement(TimestampRenderer, props));
|
||||
|
||||
expect(markup).toContain('<time');
|
||||
expect(markup).toContain('dateTime="2021-04-20T21:20:30.000Z"');
|
||||
});
|
||||
});
|
||||
@@ -20,6 +20,7 @@
|
||||
import {Tooltip} from '@app/components/uikit/tooltip/Tooltip';
|
||||
import type {RendererProps} from '@app/lib/markdown/renderers/RendererTypes';
|
||||
import {formatTimestamp} from '@app/lib/markdown/utils/DateFormatter';
|
||||
import {getDateFromUnixTimestampSeconds} from '@app/lib/markdown/utils/TimestampValidation';
|
||||
import WindowStore from '@app/stores/WindowStore';
|
||||
import markupStyles from '@app/styles/Markup.module.css';
|
||||
import timestampRendererStyles from '@app/styles/TimestampRenderer.module.css';
|
||||
@@ -32,8 +33,7 @@ import {ClockIcon} from '@phosphor-icons/react';
|
||||
import {clsx} from 'clsx';
|
||||
import {DateTime} from 'luxon';
|
||||
import {observer} from 'mobx-react-lite';
|
||||
import type {ReactElement} from 'react';
|
||||
import {useEffect, useState} from 'react';
|
||||
import React, {type ReactElement, useEffect, useState} from 'react';
|
||||
|
||||
export const TimestampRenderer = observer(function TimestampRenderer({
|
||||
node,
|
||||
@@ -43,25 +43,26 @@ export const TimestampRenderer = observer(function TimestampRenderer({
|
||||
const {timestamp, style} = node;
|
||||
const i18n = options.i18n;
|
||||
|
||||
const totalMillis = timestamp * 1000;
|
||||
const date = new Date(totalMillis);
|
||||
const date = getDateFromUnixTimestampSeconds(timestamp);
|
||||
const isValidTimestamp = date !== null;
|
||||
const now = new Date();
|
||||
|
||||
const isPast = date < now;
|
||||
const isFuture = date > now;
|
||||
const isTodayDate = isSameDay(date);
|
||||
const isPast = date !== null && date < now;
|
||||
const isFuture = date !== null && date > now;
|
||||
const isTodayDate = date !== null && isSameDay(date);
|
||||
|
||||
const locale = getCurrentLocale();
|
||||
const fullDateTime = getFormattedDateTimeWithSeconds(date, locale);
|
||||
const fullDateTime = date !== null ? getFormattedDateTimeWithSeconds(date, locale) : null;
|
||||
|
||||
const isRelativeStyle = style === TimestampStyle.RelativeTime;
|
||||
const isWindowFocused = WindowStore.focused;
|
||||
const [relativeDisplayTime, setRelativeDisplayTime] = useState(() => formatTimestamp(timestamp, style, i18n));
|
||||
const luxonDate = DateTime.fromMillis(totalMillis);
|
||||
const relativeTime = luxonDate.toRelative();
|
||||
const [relativeDisplayTime, setRelativeDisplayTime] = useState(() =>
|
||||
isValidTimestamp ? formatTimestamp(timestamp, style, i18n) : '',
|
||||
);
|
||||
const relativeTime = date !== null ? DateTime.fromJSDate(date).toRelative() : null;
|
||||
|
||||
useEffect(() => {
|
||||
if (!isRelativeStyle || !isWindowFocused) {
|
||||
if (!isValidTimestamp || !isRelativeStyle || !isWindowFocused) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -75,7 +76,11 @@ export const TimestampRenderer = observer(function TimestampRenderer({
|
||||
refreshDisplay();
|
||||
const intervalId = setInterval(refreshDisplay, 1000);
|
||||
return () => clearInterval(intervalId);
|
||||
}, [isRelativeStyle, isWindowFocused, style, timestamp, i18n]);
|
||||
}, [isValidTimestamp, isRelativeStyle, isWindowFocused, style, timestamp, i18n]);
|
||||
|
||||
if (date === null || fullDateTime === null) {
|
||||
return React.createElement('span', {className: markupStyles.timestamp}, String(timestamp));
|
||||
}
|
||||
|
||||
const tooltipContent = (
|
||||
<div className={timestampRendererStyles.tooltipContainer}>
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {formatTimestamp} from '@app/lib/markdown/utils/DateFormatter';
|
||||
import {TimestampStyle} from '@fluxer/markdown_parser/src/types/Enums';
|
||||
import {setupI18n} from '@lingui/core';
|
||||
import {afterEach, describe, expect, test, vi} from 'vitest';
|
||||
|
||||
const i18n = setupI18n({locale: 'en-US', messages: {'en-US': {}}});
|
||||
|
||||
describe('formatTimestamp', () => {
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
test('returns the raw numeric value for non-finite timestamps', () => {
|
||||
const output = formatTimestamp(Number.POSITIVE_INFINITY, TimestampStyle.ShortDateTime, i18n);
|
||||
|
||||
expect(output).toBe('Infinity');
|
||||
});
|
||||
|
||||
test('returns the raw numeric value for out-of-range timestamps', () => {
|
||||
const output = formatTimestamp(8640000000001, TimestampStyle.ShortDateTime, i18n);
|
||||
|
||||
expect(output).toBe('8640000000001');
|
||||
});
|
||||
|
||||
test('still formats valid relative timestamps', () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date('2026-02-18T12:00:00.000Z'));
|
||||
|
||||
const oneMinuteAgoTimestamp = Math.floor(new Date('2026-02-18T11:59:00.000Z').getTime() / 1000);
|
||||
const output = formatTimestamp(oneMinuteAgoTimestamp, TimestampStyle.RelativeTime, i18n);
|
||||
|
||||
expect(output.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
@@ -17,6 +17,7 @@
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {getDateFromUnixTimestampSeconds} from '@app/lib/markdown/utils/TimestampValidation';
|
||||
import {shouldUse12HourFormat} from '@app/utils/DateUtils';
|
||||
import {getCurrentLocale} from '@app/utils/LocaleUtils';
|
||||
import {
|
||||
@@ -33,9 +34,8 @@ import {TimestampStyle} from '@fluxer/markdown_parser/src/types/Enums';
|
||||
import type {I18n} from '@lingui/core';
|
||||
import {msg} from '@lingui/core/macro';
|
||||
|
||||
function formatRelativeTime(timestamp: number, i18n: I18n): string {
|
||||
function formatRelativeTime(date: Date, i18n: I18n): string {
|
||||
const locale = getCurrentLocale();
|
||||
const date = new Date(timestamp * 1000);
|
||||
const now = new Date();
|
||||
|
||||
if (isSameDay(date, now)) {
|
||||
@@ -142,9 +142,14 @@ function formatRelativeTime(timestamp: number, i18n: I18n): string {
|
||||
export function formatTimestamp(timestamp: number, style: TimestampStyle, i18n: I18n): string {
|
||||
const locale = getCurrentLocale();
|
||||
const hour12 = shouldUse12HourFormat(locale);
|
||||
const date = getDateFromUnixTimestampSeconds(timestamp);
|
||||
|
||||
if (date == null) {
|
||||
return String(timestamp);
|
||||
}
|
||||
|
||||
if (style === TimestampStyle.RelativeTime) {
|
||||
return formatRelativeTime(timestamp, i18n);
|
||||
return formatRelativeTime(date, i18n);
|
||||
}
|
||||
|
||||
return formatTimestampWithStyle(timestamp, style, locale, hour12);
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {getDateFromUnixTimestampSeconds} from '@app/lib/markdown/utils/TimestampValidation';
|
||||
import {describe, expect, test} from 'vitest';
|
||||
|
||||
describe('getDateFromUnixTimestampSeconds', () => {
|
||||
test('returns a valid date for normal unix timestamps', () => {
|
||||
const result = getDateFromUnixTimestampSeconds(1618953630);
|
||||
|
||||
expect(result).toBeInstanceOf(Date);
|
||||
expect(result?.toISOString()).toBe('2021-04-20T21:20:30.000Z');
|
||||
});
|
||||
|
||||
test('returns null for infinity', () => {
|
||||
expect(getDateFromUnixTimestampSeconds(Number.POSITIVE_INFINITY)).toBeNull();
|
||||
});
|
||||
|
||||
test('returns null for NaN', () => {
|
||||
expect(getDateFromUnixTimestampSeconds(Number.NaN)).toBeNull();
|
||||
});
|
||||
|
||||
test('returns null when timestamp is beyond js date range', () => {
|
||||
expect(getDateFromUnixTimestampSeconds(8640000000001)).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
const MILLISECONDS_PER_SECOND = 1000;
|
||||
|
||||
export function getDateFromUnixTimestampSeconds(timestamp: number): Date | null {
|
||||
if (!Number.isFinite(timestamp)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const timestampMillis = timestamp * MILLISECONDS_PER_SECOND;
|
||||
if (!Number.isFinite(timestampMillis)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const date = new Date(timestampMillis);
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return date;
|
||||
}
|
||||
Reference in new issue
Block a user