(feat): add custom lint rules
(qol): update todo (chore): update licenses
This commit is contained in:
parent
bf8f449f03
commit
17db895203
28 changed files with 585 additions and 513 deletions
|
|
@ -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 &&
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
}
|
||||
|
|
|
|||
1
packages/onboarding/todo.md
Normal file
1
packages/onboarding/todo.md
Normal file
|
|
@ -0,0 +1 @@
|
|||
- Make the legal loader use ldrs line wobble to better fit to the LoadingScreen from @tensamin/ui
|
||||
|
|
@ -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");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue