(feat): add custom lint rules
Some checks failed
/ build-web (push) Successful in 7m4s
/ build-desktop (linux) (push) Successful in 11m31s
/ build-mobile (push) Successful in 18m12s
/ release (push) Failing after 3m2s

(qol): update todo
(chore): update licenses
This commit is contained in:
Alois 2026-07-24 12:44:08 +02:00
commit 17db895203
Signed by: alois
SSH key fingerprint: SHA256:GBzT2DXvAuGV9XIV5W3WrzVpjU54FThmxHXdbz95J24
28 changed files with 585 additions and 513 deletions

View file

@ -1,8 +1,7 @@
import * as React from "react";
import type { RawMessage } from "../values";
import Text from "@tensamin/markdown/text";
import { AlertTriangle, Check, Ellipse, RefreshCw } from "lucide-react";
import { useCallback, useEffect, useState } from "react";
import { memo, useCallback, useEffect, useState } from "react";
import type { User } from "@tensamin/user/context";
import {
@ -384,7 +383,7 @@ function MessageComponent({
);
}
export default React.memo(MessageComponent, (prev, next) => {
export default memo(MessageComponent, (prev, next) => {
return (
prev.message.SendTime === next.message.SendTime &&
prev.message.Content === next.message.Content &&

View file

@ -1,4 +1,11 @@
import * as React from "react";
import {
useCallback,
useEffect,
useLayoutEffect,
useMemo,
useRef,
useState,
} from "react";
import { useInfiniteQuery } from "@tanstack/react-query";
import { useVirtualizer } from "@tanstack/react-virtual";
@ -82,21 +89,21 @@ export default function Screen() {
error,
errorDescription,
} = useChat();
const scrollRef = React.useRef<HTMLDivElement | null>(null);
const topSentinelRef = React.useRef<HTMLDivElement | null>(null);
const didInitialScrollRef = React.useRef(false);
const userScrolledUpRef = React.useRef(false);
const isAtBottomRef = React.useRef(true);
const smoothScrollFrameRef = React.useRef<number | null>(null);
const smoothScrollTargetRef = React.useRef(0);
const hasNextPageRef = React.useRef(false);
const isFetchingNextPageRef = React.useRef(false);
const fetchNextPageRef = React.useRef<(() => void) | null>(null);
const scrollRef = useRef<HTMLDivElement | null>(null);
const topSentinelRef = useRef<HTMLDivElement | null>(null);
const didInitialScrollRef = useRef(false);
const userScrolledUpRef = useRef(false);
const isAtBottomRef = useRef(true);
const smoothScrollFrameRef = useRef<number | null>(null);
const smoothScrollTargetRef = useRef(0);
const hasNextPageRef = useRef(false);
const isFetchingNextPageRef = useRef(false);
const fetchNextPageRef = useRef<(() => void) | null>(null);
const [lastLiveMessageCount, setLastLiveMessageCount] = React.useState(0);
const [didInitialScroll, setDidInitialScroll] = React.useState(false);
const [viewportHeight, setViewportHeight] = React.useState(0);
const [value, setValue] = React.useState("");
const [lastLiveMessageCount, setLastLiveMessageCount] = useState(0);
const [didInitialScroll, setDidInitialScroll] = useState(false);
const [viewportHeight, setViewportHeight] = useState(0);
const [value, setValue] = useState("");
const hasValidChatUser = Number.isSafeInteger(userId) && userId > 0;
const hasChatSecret = chatSecret !== null;
@ -120,7 +127,7 @@ export default function Screen() {
isFetchingNextPage: isFetchingMessagesNextPage,
} = messagesQuery;
React.useEffect(() => {
useEffect(() => {
hasNextPageRef.current = hasMessagesNextPage;
isFetchingNextPageRef.current = isFetchingMessagesNextPage;
fetchNextPageRef.current = () => {
@ -128,7 +135,7 @@ export default function Screen() {
};
}, [fetchMessagesNextPage, hasMessagesNextPage, isFetchingMessagesNextPage]);
React.useEffect(() => {
useEffect(() => {
clearLiveMessages();
didInitialScrollRef.current = false;
userScrolledUpRef.current = false;
@ -137,7 +144,7 @@ export default function Screen() {
setLastLiveMessageCount(0);
}, [clearLiveMessages, userId]);
const historicalMessages = React.useMemo(() => {
const historicalMessages = useMemo(() => {
const pages = messagesQuery.data?.pages ?? [];
const seenSendTimes = new Set<number>();
const dedupedMessages: RawMessage[] = [];
@ -156,7 +163,7 @@ export default function Screen() {
const liveMessagesSnapshot = liveMessages();
const liveWithoutDuplicates = React.useMemo(() => {
const liveWithoutDuplicates = useMemo(() => {
const historicalSendTimes = new Set(
historicalMessages.map((message) => message.SendTime),
);
@ -166,15 +173,15 @@ export default function Screen() {
);
}, [historicalMessages, liveMessagesSnapshot]);
const messages = React.useMemo(() => {
const messages = useMemo(() => {
return [...historicalMessages, ...liveWithoutDuplicates];
}, [historicalMessages, liveWithoutDuplicates]);
const historicalMessageChunks = React.useMemo(() => {
const historicalMessageChunks = useMemo(() => {
return buildMessageChunks(historicalMessages, "history");
}, [historicalMessages]);
const liveMessageChunks = React.useMemo(() => {
const liveMessageChunks = useMemo(() => {
return buildMessageChunks(
liveWithoutDuplicates,
"live",
@ -182,18 +189,18 @@ export default function Screen() {
);
}, [historicalMessages.length, liveWithoutDuplicates]);
const messageChunks = React.useMemo(() => {
const messageChunks = useMemo(() => {
return [...liveMessageChunks, ...historicalMessageChunks];
}, [historicalMessageChunks, liveMessageChunks]);
const virtualRowCount = messageChunks.length;
const getItemKey = React.useCallback(
const getItemKey = useCallback(
(index: number) => messageChunks[index]?.key ?? index,
[messageChunks],
);
const estimateSize = React.useCallback(() => FALLBACK_MESSAGE_HEIGHT, []);
const estimateSize = useCallback(() => FALLBACK_MESSAGE_HEIGHT, []);
// eslint-disable-next-line react-hooks/incompatible-library
const virtualizer = useVirtualizer({
@ -207,7 +214,7 @@ export default function Screen() {
const contentHeight = Math.max(totalSize, viewportHeight);
const verticalOffset = Math.max(0, viewportHeight - totalSize);
React.useLayoutEffect(() => {
useLayoutEffect(() => {
const element = scrollRef.current;
if (!element || typeof ResizeObserver === "undefined") {
return;
@ -227,7 +234,7 @@ export default function Screen() {
};
}, []);
React.useLayoutEffect(() => {
useLayoutEffect(() => {
if (didInitialScrollRef.current || virtualRowCount === 0) {
return;
}
@ -244,7 +251,7 @@ export default function Screen() {
});
}, [virtualizer, virtualRowCount]);
React.useLayoutEffect(() => {
useLayoutEffect(() => {
const count = liveMessagesSnapshot.length;
if (count > lastLiveMessageCount && isAtBottomRef.current) {
@ -263,7 +270,7 @@ export default function Screen() {
virtualizer,
]);
React.useEffect(() => {
useEffect(() => {
if (
viewportHeight === 0 ||
totalSize > viewportHeight ||
@ -276,7 +283,7 @@ export default function Screen() {
void messagesQuery.fetchNextPage();
}, [didInitialScroll, messagesQuery, totalSize, viewportHeight]);
React.useLayoutEffect(() => {
useLayoutEffect(() => {
if (
!didInitialScroll ||
userScrolledUpRef.current ||
@ -292,7 +299,7 @@ export default function Screen() {
});
}, [didInitialScroll, virtualRowCount]);
React.useEffect(() => {
useEffect(() => {
const root = scrollRef.current;
const sentinel = topSentinelRef.current;
@ -324,7 +331,7 @@ export default function Screen() {
};
}, [didInitialScroll, messagesQuery, virtualRowCount]);
const handleContainerScroll = React.useCallback(() => {
const handleContainerScroll = useCallback(() => {
if (!scrollRef.current) {
return;
}
@ -336,7 +343,7 @@ export default function Screen() {
}
}, []);
React.useEffect(() => {
useEffect(() => {
const element = scrollRef.current;
if (!element) {
return;

View file

@ -1,4 +1,4 @@
import * as React from "react";
import { Fragment, type ReactElement, type ReactNode } from "react";
import Emoji from "./emoji";
import { findEmojiShortcodes } from "./emojiData";
@ -392,7 +392,7 @@ export function parseMarkdownBlocks(markdown: string): MarkdownBlock[] {
* @param nodes Parameter nodes.
* @returns React.ReactNode[].
*/
function renderInline(nodes: InlineNode[]): React.ReactNode[] {
function renderInline(nodes: InlineNode[]): ReactNode[] {
return nodes.map((node, index) => {
if (node.type === "text") {
return node.value;
@ -468,7 +468,7 @@ function renderInline(nodes: InlineNode[]): React.ReactNode[] {
* @param blocks Parameter blocks.
* @returns React.ReactElement.
*/
export function renderBlocks(blocks: MarkdownBlock[]): React.ReactElement {
export function renderBlocks(blocks: MarkdownBlock[]): ReactElement {
return (
<>
{blocks.map((block, blockIndex) => {
@ -591,10 +591,10 @@ export function renderBlocks(blocks: MarkdownBlock[]): React.ReactElement {
return (
<p key={blockIndex}>
{block.text.split("\n").map((line, lineIndex) => (
<React.Fragment key={lineIndex}>
<Fragment key={lineIndex}>
{lineIndex > 0 ? <br /> : null}
{renderInline(parseInlineNodes(line))}
</React.Fragment>
</Fragment>
))}
</p>
);

View file

@ -1,4 +1,4 @@
import * as React from "react";
import { useMemo } from "react";
import {
ensureMarkdownStyles,
@ -18,11 +18,8 @@ export type TextProps = {
export default function Text(props: TextProps) {
ensureMarkdownStyles();
const blocks = React.useMemo(
() => parseMarkdownBlocks(props.value),
[props.value],
);
const renderedBlocks = React.useMemo(() => renderBlocks(blocks), [blocks]);
const blocks = useMemo(() => parseMarkdownBlocks(props.value), [props.value]);
const renderedBlocks = useMemo(() => renderBlocks(blocks), [blocks]);
return <div className="tm-md-root">{renderedBlocks}</div>;
}

View file

@ -0,0 +1 @@
- Make the legal loader use ldrs line wobble to better fit to the LoadingScreen from @tensamin/ui

View file

@ -1,4 +1,13 @@
import * as React from "react";
import {
createContext,
type ReactNode,
useCallback,
useContext,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import {
type Storage as StorageSchema,
storageDefaults as defaults,
@ -31,7 +40,7 @@ interface StorageContextValue {
secureStorage: SecureStorageStatus | null;
}
const StorageContext = React.createContext<StorageContextValue | undefined>(
const StorageContext = createContext<StorageContextValue | undefined>(
undefined,
);
@ -42,29 +51,29 @@ const isIndexedDBSupported = typeof indexedDB !== "undefined";
* @param props Parameter props.
* @returns unknown.
*/
export default function StorageProvider(props: { children: React.ReactNode }) {
const [storage, setStorage] = React.useState<StorageSchema>(defaults);
const storageRef = React.useRef(storage);
const loadedKeys = React.useRef(new Set<keyof StorageSchema>());
const loadPromises = React.useRef(
export default function StorageProvider(props: { children: ReactNode }) {
const [storage, setStorage] = useState<StorageSchema>(defaults);
const storageRef = useRef(storage);
const loadedKeys = useRef(new Set<keyof StorageSchema>());
const loadPromises = useRef(
new Map<keyof StorageSchema, Promise<StorageSchema[keyof StorageSchema]>>(),
);
const generations = React.useRef(new Map<keyof StorageSchema, number>());
const generations = useRef(new Map<keyof StorageSchema, number>());
const [secureStorage, setSecureStorage] =
React.useState<SecureStorageStatus | null>(null);
const secureStorageRef = React.useRef<SecureStorageStatus | null>(null);
useState<SecureStorageStatus | null>(null);
const secureStorageRef = useRef<SecureStorageStatus | null>(null);
const [error, setError] = React.useState("");
const [errorDescription, setErrorDescription] = React.useState("");
const [error, setError] = useState("");
const [errorDescription, setErrorDescription] = useState("");
React.useEffect(() => {
useEffect(() => {
void getSecureStorageStatus().then((status) => {
secureStorageRef.current = status;
setSecureStorage(status);
});
}, []);
const commit = React.useCallback(
const commit = useCallback(
<K extends keyof StorageSchema>(key: K, nextValue: StorageSchema[K]) => {
const next = { ...storageRef.current, [key]: nextValue };
storageRef.current = next;
@ -74,7 +83,7 @@ export default function StorageProvider(props: { children: React.ReactNode }) {
[],
);
const load = React.useCallback(
const load = useCallback(
async <K extends keyof StorageSchema>(
key: K,
): Promise<StorageSchema[K]> => {
@ -127,7 +136,7 @@ export default function StorageProvider(props: { children: React.ReactNode }) {
[commit],
);
const save = React.useCallback(
const save = useCallback(
async <K extends keyof StorageSchema>(
key: K,
value: StorageSchema[K],
@ -163,14 +172,12 @@ export default function StorageProvider(props: { children: React.ReactNode }) {
[commit],
);
const clear = React.useCallback(async () => {
const clear = useCallback(async () => {
const keys = Object.keys(defaults) as (keyof StorageSchema)[];
for (const key of keys) {
generations.current.set(key, (generations.current.get(key) ?? 0) + 1);
}
await Promise.all(
keys.map((key) => deleteDatabaseEntry("storage", key)),
);
await Promise.all(keys.map((key) => deleteDatabaseEntry("storage", key)));
await window.tensaminDesktop?.secureStorage
?.clear?.()
.catch(() => undefined);
@ -180,7 +187,7 @@ export default function StorageProvider(props: { children: React.ReactNode }) {
setStorage(defaults);
}, []);
const value = React.useMemo<StorageContextValue>(
const value = useMemo<StorageContextValue>(
() => ({
load,
save,
@ -216,7 +223,7 @@ export default function StorageProvider(props: { children: React.ReactNode }) {
* @returns StorageContextValue.
*/
export function useStorage(): StorageContextValue {
const context = React.useContext(StorageContext);
const context = useContext(StorageContext);
if (!context) {
throw new Error("useStorage must be used within a StorageProvider");
}

View file

@ -1,4 +1,12 @@
import * as React from "react";
import {
createContext,
type ReactNode,
useCallback,
useContext,
useEffect,
useRef,
useState,
} from "react";
import { useMTP } from "@tensamin/mtp";
import { mtp as schemas } from "@tensamin/shared/data";
@ -13,25 +21,23 @@ interface contextValue {
get(userId: number): Promise<User>;
}
const UserContext = React.createContext<contextValue | undefined>(undefined);
const UserContext = createContext<contextValue | undefined>(undefined);
/**
* Executes UserProvider.
* @param props Parameter props.
* @returns unknown.
*/
export default function UserProvider(props: { children: React.ReactNode }) {
const storageRef = React.useRef<Record<number, User>>({});
const pendingRef = React.useRef<Record<number, Promise<User> | undefined>>(
{},
);
export default function UserProvider(props: { children: ReactNode }) {
const storageRef = useRef<Record<number, User>>({});
const pendingRef = useRef<Record<number, Promise<User> | undefined>>({});
const { send } = useMTP();
const { load } = useStorage();
const { contacts } = useSession();
const [accountId, setAccountId] = React.useState<number | null>(null);
const [accountId, setAccountId] = useState<number | null>(null);
React.useEffect(() => {
useEffect(() => {
void load("user_id").then((accountId) => {
setAccountId(accountId);
});
@ -42,7 +48,7 @@ export default function UserProvider(props: { children: React.ReactNode }) {
* @param userId Parameter userId.
* @returns Promise<User>.
*/
const get = React.useCallback(
const get = useCallback(
async (userId: number): Promise<User> => {
if (userId == null) {
throw new Error("userId is required");
@ -85,7 +91,7 @@ export default function UserProvider(props: { children: React.ReactNode }) {
[accountId, send],
);
React.useEffect(() => {
useEffect(() => {
if (!accountId) return;
for (const contact of contacts) void get(contact.UserId);
}, [accountId, contacts, get]);
@ -103,7 +109,7 @@ export default function UserProvider(props: { children: React.ReactNode }) {
* @returns contextValue.
*/
export function useUser(): contextValue {
const context = React.useContext(UserContext);
const context = useContext(UserContext);
if (!context) {
throw new Error("useUser must be used within a UserProvider");
}