(feat): big methanium/ui migration
(feat): update message state icons (qol): update todo (qol): formatted
This commit is contained in:
parent
5e79893137
commit
ace7973dff
24 changed files with 842 additions and 541 deletions
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
[submodule "mtp-type-maps"]
|
||||||
|
path = mtp-type-maps
|
||||||
|
url = https://git.methanium.net/tensamin/mtp-type-maps
|
||||||
1
TODO
1
TODO
|
|
@ -1,5 +1,6 @@
|
||||||
- Add a bunch of tests
|
- Add a bunch of tests
|
||||||
- Add packages/hotkeys/
|
- Add packages/hotkeys/
|
||||||
- Full accessability
|
- Full accessability
|
||||||
|
- Add settings saving & update onboarding to use it
|
||||||
|
|
||||||
-> Most folders in packages/ have specific todos
|
-> Most folders in packages/ have specific todos
|
||||||
|
|
|
||||||
|
|
@ -8,14 +8,12 @@ const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = path.dirname(__filename);
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
||||||
function getTrayIconPath(filename: string) {
|
function getTrayIconPath(filename: string) {
|
||||||
if (app.isPackaged) return path.join(process.resourcesPath, "icons", filename);
|
if (app.isPackaged)
|
||||||
|
return path.join(process.resourcesPath, "icons", filename);
|
||||||
return path.resolve(__dirname, "../../build/icons", filename);
|
return path.resolve(__dirname, "../../build/icons", filename);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setTrayCallStatus(
|
export function setTrayCallStatus(inCall: boolean, iconDataUrl?: string) {
|
||||||
inCall: boolean,
|
|
||||||
iconDataUrl?: string,
|
|
||||||
) {
|
|
||||||
if (!tray) return;
|
if (!tray) return;
|
||||||
|
|
||||||
if (inCall && iconDataUrl) {
|
if (inCall && iconDataUrl) {
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,12 @@ import { useLocation, useNavigate, useSearch } from "@tanstack/react-router";
|
||||||
import { joinCall, useCall } from "@tensamin/call/store";
|
import { joinCall, useCall } from "@tensamin/call/store";
|
||||||
import Wrapper from "@tensamin/user/wrapper";
|
import Wrapper from "@tensamin/user/wrapper";
|
||||||
import { Skeleton } from "@methanium/ui";
|
import { Skeleton } from "@methanium/ui";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger } from "@methanium/ui";
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
} from "@methanium/ui";
|
||||||
import { displayCallId } from "@tensamin/call/utils";
|
import { displayCallId } from "@tensamin/call/utils";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { SidebarTrigger, useSidebar } from "@methanium/ui";
|
import { SidebarTrigger, useSidebar } from "@methanium/ui";
|
||||||
|
|
@ -43,7 +48,7 @@ export default function Navbar({ forMobile }: { forMobile: boolean }) {
|
||||||
<div className="flex items-center justify-center gap-2">
|
<div className="flex items-center justify-center gap-2">
|
||||||
{forMobile ? (
|
{forMobile ? (
|
||||||
<SidebarTrigger
|
<SidebarTrigger
|
||||||
className="w-9 h-9 aspect-square rounded-lg ml-2"
|
className="w-9 h-9! aspect-square rounded-lg ml-2 bg-(--input)/35!"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
>
|
>
|
||||||
<ArrowLeft className="size-4.5" />
|
<ArrowLeft className="size-4.5" />
|
||||||
|
|
@ -52,14 +57,14 @@ export default function Navbar({ forMobile }: { forMobile: boolean }) {
|
||||||
<>
|
<>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => navigate({ to: "/" })}
|
onClick={() => navigate({ to: "/" })}
|
||||||
className="w-9 h-9 aspect-square rounded-lg"
|
className="w-9 h-9! aspect-square rounded-lg bg-(--input)/35!"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
>
|
>
|
||||||
<House className="size-4.5" />
|
<House className="size-4.5" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => navigate({ to: "/settings" })}
|
onClick={() => navigate({ to: "/settings" })}
|
||||||
className="w-9 h-9 aspect-square rounded-lg"
|
className="w-9 h-9! aspect-square rounded-lg bg-(--input)/35!"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
>
|
>
|
||||||
<Settings className="size-4.5" />
|
<Settings className="size-4.5" />
|
||||||
|
|
@ -109,7 +114,7 @@ export default function Navbar({ forMobile }: { forMobile: boolean }) {
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
void joinCall(id);
|
void joinCall(id);
|
||||||
}}
|
}}
|
||||||
className="w-9 h-9 aspect-square rounded-lg"
|
className="w-9 h-9! aspect-square rounded-lg bg-(--input)/35!"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
>
|
>
|
||||||
<Phone />
|
<Phone />
|
||||||
|
|
@ -124,7 +129,7 @@ export default function Navbar({ forMobile }: { forMobile: boolean }) {
|
||||||
currentCalls[0].CallId,
|
currentCalls[0].CallId,
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
className="w-9 h-9 aspect-square rounded-lg"
|
className="w-9 h-9! aspect-square rounded-lg bg-(--input)/35!"
|
||||||
>
|
>
|
||||||
<Phone />
|
<Phone />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -175,7 +180,7 @@ export function MobileNavbar() {
|
||||||
<div className="z-51 w-full shrink-0 pb-[env(safe-area-inset-bottom)] bg-card border-t">
|
<div className="z-51 w-full shrink-0 pb-[env(safe-area-inset-bottom)] bg-card border-t">
|
||||||
<div className="z-51 w-full h-15 grid grid-cols-3 items-center place-items-center px-5">
|
<div className="z-51 w-full h-15 grid grid-cols-3 items-center place-items-center px-5">
|
||||||
<SidebarTrigger
|
<SidebarTrigger
|
||||||
className="text-foreground w-12! h-12! aspect-square rounded-xl flex flex-col gap-1"
|
className="text-foreground w-12! h-12! aspect-square rounded-xl flex flex-col gap-1 border-0!"
|
||||||
variant="link"
|
variant="link"
|
||||||
>
|
>
|
||||||
<User className="size-4.5" />
|
<User className="size-4.5" />
|
||||||
|
|
@ -186,7 +191,7 @@ export function MobileNavbar() {
|
||||||
navigate({ to: "/" });
|
navigate({ to: "/" });
|
||||||
setOpenMobile(false);
|
setOpenMobile(false);
|
||||||
}}
|
}}
|
||||||
className="text-foreground w-12 h-12 aspect-square rounded-xl flex flex-col gap-1"
|
className="text-foreground w-12 h-12 aspect-square rounded-xl flex flex-col gap-1 border-0! hover:bg-transparent!"
|
||||||
variant="link"
|
variant="link"
|
||||||
>
|
>
|
||||||
<House className="size-4.5" />
|
<House className="size-4.5" />
|
||||||
|
|
@ -197,7 +202,7 @@ export function MobileNavbar() {
|
||||||
navigate({ to: "/settings" });
|
navigate({ to: "/settings" });
|
||||||
setOpenMobile(false);
|
setOpenMobile(false);
|
||||||
}}
|
}}
|
||||||
className="text-foreground w-12 h-12 aspect-square rounded-xl flex flex-col gap-1"
|
className="text-foreground w-12 h-12 aspect-square rounded-xl flex flex-col gap-1 border-0! hover:bg-transparent!"
|
||||||
variant="link"
|
variant="link"
|
||||||
>
|
>
|
||||||
<Settings className="size-4.5" />
|
<Settings className="size-4.5" />
|
||||||
|
|
|
||||||
|
|
@ -131,6 +131,11 @@ function ThemeStorageBridge() {
|
||||||
setThemeBorderRadius,
|
setThemeBorderRadius,
|
||||||
themeCustomCss,
|
themeCustomCss,
|
||||||
setThemeCustomCss,
|
setThemeCustomCss,
|
||||||
|
parentThemeId,
|
||||||
|
setParentThemeId,
|
||||||
|
applyThemePreset,
|
||||||
|
themeDesign,
|
||||||
|
setThemeDesign,
|
||||||
} = useTheme();
|
} = useTheme();
|
||||||
const loadedRef = useRef(false);
|
const loadedRef = useRef(false);
|
||||||
|
|
||||||
|
|
@ -145,6 +150,8 @@ function ThemeStorageBridge() {
|
||||||
load("theme_tint"),
|
load("theme_tint"),
|
||||||
load("theme_border_radius"),
|
load("theme_border_radius"),
|
||||||
load("theme_custom_css"),
|
load("theme_custom_css"),
|
||||||
|
load("theme_parent"),
|
||||||
|
load("theme_design"),
|
||||||
]).then(
|
]).then(
|
||||||
([
|
([
|
||||||
color,
|
color,
|
||||||
|
|
@ -154,18 +161,23 @@ function ThemeStorageBridge() {
|
||||||
tint,
|
tint,
|
||||||
borderRadius,
|
borderRadius,
|
||||||
customCss,
|
customCss,
|
||||||
|
parent,
|
||||||
|
design,
|
||||||
]) => {
|
]) => {
|
||||||
if (!active) {
|
if (!active) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (customCss === "" && parent) applyThemePreset(parent);
|
||||||
|
else setParentThemeId(parent || null);
|
||||||
setThemeColor(color);
|
setThemeColor(color);
|
||||||
setThemePalette(palette);
|
setThemePalette(palette);
|
||||||
setThemePrimaryColor(primaryColor);
|
setThemePrimaryColor(primaryColor);
|
||||||
setThemePolarity(polarity);
|
setThemePolarity(polarity);
|
||||||
setThemeTint(tint);
|
setThemeTint(tint);
|
||||||
setThemeBorderRadius(borderRadius);
|
setThemeBorderRadius(borderRadius);
|
||||||
setThemeCustomCss(customCss);
|
if (customCss !== "") setThemeCustomCss(customCss);
|
||||||
|
setThemeDesign(design);
|
||||||
loadedRef.current = true;
|
loadedRef.current = true;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
@ -175,9 +187,12 @@ function ThemeStorageBridge() {
|
||||||
};
|
};
|
||||||
}, [
|
}, [
|
||||||
load,
|
load,
|
||||||
|
applyThemePreset,
|
||||||
setThemeBorderRadius,
|
setThemeBorderRadius,
|
||||||
setThemeColor,
|
setThemeColor,
|
||||||
setThemeCustomCss,
|
setThemeCustomCss,
|
||||||
|
setParentThemeId,
|
||||||
|
setThemeDesign,
|
||||||
setThemePalette,
|
setThemePalette,
|
||||||
setThemePolarity,
|
setThemePolarity,
|
||||||
setThemePrimaryColor,
|
setThemePrimaryColor,
|
||||||
|
|
@ -212,6 +227,14 @@ function ThemeStorageBridge() {
|
||||||
if (loadedRef.current) save("theme_custom_css", themeCustomCss);
|
if (loadedRef.current) save("theme_custom_css", themeCustomCss);
|
||||||
}, [save, themeCustomCss]);
|
}, [save, themeCustomCss]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (loadedRef.current) save("theme_parent", parentThemeId ?? "");
|
||||||
|
}, [parentThemeId, save]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (loadedRef.current) save("theme_design", themeDesign);
|
||||||
|
}, [save, themeDesign]);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -228,6 +251,8 @@ function RootShell() {
|
||||||
tintStorageKey={null}
|
tintStorageKey={null}
|
||||||
borderRadiusStorageKey={null}
|
borderRadiusStorageKey={null}
|
||||||
customCssStorageKey={null}
|
customCssStorageKey={null}
|
||||||
|
parentThemeStorageKey={null}
|
||||||
|
designStorageKey={null}
|
||||||
>
|
>
|
||||||
<div className="w-screen h-dvh overflow-hidden">
|
<div className="w-screen h-dvh overflow-hidden">
|
||||||
<Toaster
|
<Toaster
|
||||||
|
|
|
||||||
|
|
@ -131,7 +131,11 @@ function AddConversationButton() {
|
||||||
setOpen(value);
|
setOpen(value);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DialogTrigger render={<Button>Add Conversation</Button>} />
|
<DialogTrigger
|
||||||
|
render={({ onClick }) => (
|
||||||
|
<Button onClick={onClick}>Add Conversation</Button>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>New Conversation</DialogTitle>
|
<DialogTitle>New Conversation</DialogTitle>
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import { defineConfig, type Plugin } from "vite";
|
||||||
import react from "@vitejs/plugin-react";
|
import react from "@vitejs/plugin-react";
|
||||||
import tailwindcss from "@tailwindcss/vite";
|
import tailwindcss from "@tailwindcss/vite";
|
||||||
import { mtp } from "mtp/vite";
|
import { mtp } from "mtp/vite";
|
||||||
|
import { methaniumUi } from "@methanium/ui/vite";
|
||||||
|
|
||||||
const host = process.env.TAURI_DEV_HOST;
|
const host = process.env.TAURI_DEV_HOST;
|
||||||
const appDir = dirname(fileURLToPath(import.meta.url));
|
const appDir = dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
@ -153,8 +154,9 @@ export default defineConfig({
|
||||||
sourcemap: !!process.env.TAURI_ENV_DEBUG,
|
sourcemap: !!process.env.TAURI_ENV_DEBUG,
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
|
methaniumUi({ defaultThemeId: "tensamin" }),
|
||||||
deepFilterAssetHeaders(resolve(appDir, "public")),
|
deepFilterAssetHeaders(resolve(appDir, "public")),
|
||||||
mtp({ typeMaps: resolve(appDir, "../../type-maps.yaml") }),
|
mtp({ typeMaps: resolve(appDir, "../../mtp-type-maps/type-maps.yaml") }),
|
||||||
{
|
{
|
||||||
name: "workspace-realpath-resolution",
|
name: "workspace-realpath-resolution",
|
||||||
enforce: "post",
|
enforce: "post",
|
||||||
|
|
|
||||||
1
mtp-type-maps
Submodule
1
mtp-type-maps
Submodule
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 6d97b5b9935b15d957444d38609378c66a24262a
|
||||||
|
|
@ -46,7 +46,7 @@
|
||||||
"yaml": "^2.8.2"
|
"yaml": "^2.8.2"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@methanium/ui": "*",
|
"@methanium/ui": "https://git.methanium.net/methanium/ui/releases/download/0.0.2/methanium-ui.tgz",
|
||||||
"mtp": "*",
|
"mtp": "*",
|
||||||
"sonner": "^2.0.7"
|
"sonner": "^2.0.7"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
21
packages/cache/src/index.ts
vendored
21
packages/cache/src/index.ts
vendored
|
|
@ -175,12 +175,7 @@ export function createCache(accountId: string, options: CacheOptions = {}) {
|
||||||
const userId = Number(key);
|
const userId = Number(key);
|
||||||
return retained.has(userId)
|
return retained.has(userId)
|
||||||
? []
|
? []
|
||||||
: [
|
: [deleteDatabaseEntry("cache", storedKey("conversations", key))];
|
||||||
deleteDatabaseEntry(
|
|
||||||
"cache",
|
|
||||||
storedKey("conversations", key),
|
|
||||||
),
|
|
||||||
];
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -210,12 +205,7 @@ export function createCache(accountId: string, options: CacheOptions = {}) {
|
||||||
const userId = Number(key);
|
const userId = Number(key);
|
||||||
return retained.has(userId)
|
return retained.has(userId)
|
||||||
? []
|
? []
|
||||||
: [
|
: [deleteDatabaseEntry("cache", storedKey("conversations", key))];
|
||||||
deleteDatabaseEntry(
|
|
||||||
"cache",
|
|
||||||
storedKey("conversations", key),
|
|
||||||
),
|
|
||||||
];
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -241,12 +231,7 @@ export function createCache(accountId: string, options: CacheOptions = {}) {
|
||||||
storedEntries.flatMap(([key]) =>
|
storedEntries.flatMap(([key]) =>
|
||||||
retained.has(Number(key))
|
retained.has(Number(key))
|
||||||
? []
|
? []
|
||||||
: [
|
: [deleteDatabaseEntry("cache", storedKey("conversations", key))],
|
||||||
deleteDatabaseEntry(
|
|
||||||
"cache",
|
|
||||||
storedKey("conversations", key),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import type { RawMessage } from "../values";
|
import type { RawMessage } from "../values";
|
||||||
import Text from "@tensamin/markdown/text";
|
import Text from "@tensamin/markdown/text";
|
||||||
import { AlertTriangle, Check, Ellipse, RefreshCw } from "lucide-react";
|
import { AlertTriangle, Check, CheckLine, RefreshCw } from "lucide-react";
|
||||||
import { memo, useCallback, useEffect, useState } from "react";
|
import { memo, useCallback, useEffect, useState } from "react";
|
||||||
import type { User } from "@tensamin/user/context";
|
import type { User } from "@tensamin/user/context";
|
||||||
|
|
||||||
|
|
@ -289,11 +289,11 @@ function MessageComponent({
|
||||||
color="var(--primary-foreground-alt)"
|
color="var(--primary-foreground-alt)"
|
||||||
/>
|
/>
|
||||||
) : message.MessageState === "received" ? (
|
) : message.MessageState === "received" ? (
|
||||||
<Check size={12} color="var(--muted-foreground)" />
|
<CheckLine size={12} color="var(--muted-foreground)" />
|
||||||
) : message.MessageState === "sent" ? (
|
) : message.MessageState === "sent" ? (
|
||||||
<RefreshCw size={12} color="var(--muted-foreground)" />
|
<Check size={12} color="var(--muted-foreground)" />
|
||||||
) : message.MessageState === "sending" ? (
|
) : message.MessageState === "sending" ? (
|
||||||
<Ellipse size={12} color="var(--muted-foreground)" />
|
<RefreshCw size={12} color="var(--muted-foreground)" />
|
||||||
) : null}
|
) : null}
|
||||||
{actuallyFailed && (
|
{actuallyFailed && (
|
||||||
<AlertTriangle color="var(--destructive)" size={12} />
|
<AlertTriangle color="var(--destructive)" size={12} />
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,9 @@ export default function OnboardingGate({ children }: { children: ReactNode }) {
|
||||||
const [showLoading, setShowLoading] = useState(false);
|
const [showLoading, setShowLoading] = useState(false);
|
||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
const [errorDescription, setErrorDescription] = useState("");
|
const [errorDescription, setErrorDescription] = useState("");
|
||||||
|
const [onboardingThemeId, setOnboardingThemeId] = useState<string | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let active = true;
|
let active = true;
|
||||||
|
|
@ -63,13 +66,19 @@ export default function OnboardingGate({ children }: { children: ReactNode }) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [localDocs, acceptedPP, acceptedTOS, onboardingDone] =
|
const [
|
||||||
await Promise.all([
|
localDocs,
|
||||||
load("legal_docs"),
|
acceptedPP,
|
||||||
load("accepted_privacy_policy"),
|
acceptedTOS,
|
||||||
load("accepted_terms_of_service"),
|
onboardingDone,
|
||||||
load("onboarding_done"),
|
onboardingStarted,
|
||||||
]);
|
] = await Promise.all([
|
||||||
|
load("legal_docs"),
|
||||||
|
load("accepted_privacy_policy"),
|
||||||
|
load("accepted_terms_of_service"),
|
||||||
|
load("onboarding_done"),
|
||||||
|
load("onboarding_started"),
|
||||||
|
]);
|
||||||
|
|
||||||
if (!active) return;
|
if (!active) return;
|
||||||
|
|
||||||
|
|
@ -78,11 +87,15 @@ export default function OnboardingGate({ children }: { children: ReactNode }) {
|
||||||
const currentAcceptedTOS =
|
const currentAcceptedTOS =
|
||||||
acceptedTOS && localDocs.tos.hash === parsed.data.tos.hash;
|
acceptedTOS && localDocs.tos.hash === parsed.data.tos.hash;
|
||||||
const existingUser = acceptedPP && acceptedTOS;
|
const existingUser = acceptedPP && acceptedTOS;
|
||||||
const includeOnboarding = !onboardingDone && !existingUser;
|
const includeOnboarding =
|
||||||
|
!onboardingDone && (!existingUser || onboardingStarted);
|
||||||
|
|
||||||
if (!onboardingDone && existingUser) {
|
if (!onboardingDone && existingUser && !onboardingStarted) {
|
||||||
await save("onboarding_done", true);
|
await save("onboarding_done", true);
|
||||||
if (!active) return;
|
if (!active) return;
|
||||||
|
} else if (includeOnboarding && !onboardingStarted) {
|
||||||
|
await save("onboarding_started", true);
|
||||||
|
if (!active) return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setState({
|
setState({
|
||||||
|
|
@ -124,7 +137,10 @@ export default function OnboardingGate({ children }: { children: ReactNode }) {
|
||||||
|
|
||||||
const finish = useCallback(async () => {
|
const finish = useCallback(async () => {
|
||||||
if (state?.includeOnboarding) {
|
if (state?.includeOnboarding) {
|
||||||
await save("onboarding_done", true);
|
await Promise.all([
|
||||||
|
save("onboarding_done", true),
|
||||||
|
save("onboarding_started", false),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
setComplete(true);
|
setComplete(true);
|
||||||
}, [save, state?.includeOnboarding]);
|
}, [save, state?.includeOnboarding]);
|
||||||
|
|
@ -142,6 +158,8 @@ export default function OnboardingGate({ children }: { children: ReactNode }) {
|
||||||
if (state.includeLegal) {
|
if (state.includeLegal) {
|
||||||
steps.push({
|
steps.push({
|
||||||
id: "legal",
|
id: "legal",
|
||||||
|
title: "Privacy Policy & ToS",
|
||||||
|
description: `${state.docs.pp.version} / ${state.docs.tos.version}`,
|
||||||
defaultCanContinue: false,
|
defaultCanContinue: false,
|
||||||
content: (
|
content: (
|
||||||
<LegalPage
|
<LegalPage
|
||||||
|
|
@ -154,7 +172,7 @@ export default function OnboardingGate({ children }: { children: ReactNode }) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (state.includeOnboarding) {
|
if (state.includeOnboarding) {
|
||||||
steps.push(...onboardingSteps);
|
steps.push(...onboardingSteps(onboardingThemeId, setOnboardingThemeId));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (complete || steps.length === 0) return <>{children}</>;
|
if (complete || steps.length === 0) return <>{children}</>;
|
||||||
|
|
|
||||||
|
|
@ -32,16 +32,7 @@ export default function LegalPage({
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto flex h-full w-full max-w-5xl flex-col gap-10 p-10 py-20 md:p-24">
|
<div className="mx-auto flex min-h-[calc(100dvh-17rem)] w-full max-w-5xl flex-col gap-10 p-10 py-20 md:min-h-[calc(100dvh-20.5rem)] md:p-24">
|
||||||
<div>
|
|
||||||
<h1 className="text-3xl font-bold md:text-4xl" tabIndex={-1}>
|
|
||||||
Privacy Policy & ToS
|
|
||||||
</h1>
|
|
||||||
<p className="pt-3 text-xl text-muted-foreground">
|
|
||||||
{docs.pp.version} / {docs.tos.version}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-1 items-center justify-center">
|
<div className="flex flex-1 items-center justify-center">
|
||||||
<div className="flex flex-col items-start gap-2">
|
<div className="flex flex-col items-start gap-2">
|
||||||
<BigCheckbox
|
<BigCheckbox
|
||||||
|
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
export default function OnboardingPage() {
|
|
||||||
return (
|
|
||||||
<div className="flex h-full items-center justify-center p-8">
|
|
||||||
<h1 className="text-3xl font-bold" tabIndex={-1}>
|
|
||||||
Onboarding
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,10 +1,15 @@
|
||||||
import type { OnboardingStep } from "@methanium/ui";
|
import { ThemeOnboardingPage, type OnboardingStep } from "@methanium/ui";
|
||||||
import OnboardingPage from "./pages/onboarding";
|
|
||||||
|
|
||||||
// Add future onboarding pages here. Completed users skip this entire manifest.
|
export const onboardingSteps = (
|
||||||
export const onboardingSteps: OnboardingStep[] = [
|
themeId: string | null,
|
||||||
|
setThemeId: (id: string) => void,
|
||||||
|
): OnboardingStep[] => [
|
||||||
{
|
{
|
||||||
id: "onboarding",
|
id: "theme",
|
||||||
content: <OnboardingPage />,
|
title: "Theming",
|
||||||
|
description:
|
||||||
|
"Pick a main theme and after that ... let's just hope you don't get into decision paralysis",
|
||||||
|
defaultCanContinue: false,
|
||||||
|
content: <ThemeOnboardingPage value={themeId} onValueChange={setThemeId} />,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -27,12 +27,16 @@ export default function SettingsLayout() {
|
||||||
export function SettingsSidebar() {
|
export function SettingsSidebar() {
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const categories = [...new Set(settingsNavigation.map((page) => page.category))];
|
const categories = [
|
||||||
|
...new Set(settingsNavigation.map((page) => page.category)),
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
isMobile ? "w-full p-1" : "p-3 rounded-tl-2xl border-r bg-input/15 w-50",
|
isMobile
|
||||||
|
? "w-full p-1"
|
||||||
|
: "p-3 rounded-tl-2xl border-r bg-input/15 w-50",
|
||||||
"flex flex-col gap-6",
|
"flex flex-col gap-6",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -1,32 +1,144 @@
|
||||||
import { Badge, Button, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, Dialog, DialogContent, DialogTrigger } from "@methanium/ui";
|
import {
|
||||||
import { generatedAt, packageCount, packages } from "../../../../licenses/third-party-credits.json";
|
Badge,
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardFooter,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@methanium/ui";
|
||||||
|
import {
|
||||||
|
generatedAt,
|
||||||
|
packageCount,
|
||||||
|
packages,
|
||||||
|
} from "../../../../licenses/third-party-credits.json";
|
||||||
|
|
||||||
const licenseTexts = import.meta.glob("../../../../licenses/**/*", { eager: true, import: "default", query: "?raw" }) as Record<string, string>;
|
const licenseTexts = import.meta.glob("../../../../licenses/**/*", {
|
||||||
|
eager: true,
|
||||||
|
import: "default",
|
||||||
|
query: "?raw",
|
||||||
|
}) as Record<string, string>;
|
||||||
|
|
||||||
function getLicenseFiles(licensePackage: (typeof packages)[number]) {
|
function getLicenseFiles(licensePackage: (typeof packages)[number]) {
|
||||||
return licensePackage.files.map((fileName) => ({
|
return licensePackage.files.map((fileName) => ({
|
||||||
fileName,
|
fileName,
|
||||||
text: licenseTexts["../../../../" + licensePackage.licenseFolder + "/" + fileName],
|
text: licenseTexts[
|
||||||
|
"../../../../" + licensePackage.licenseFolder + "/" + fileName
|
||||||
|
],
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
return <div className="flex h-full min-h-0 flex-col gap-7">
|
return (
|
||||||
<div className="flex flex-col"><p>Last generated: {generatedAt}</p><p>Package Count: {packageCount}</p></div>
|
<div className="flex h-full min-h-0 flex-col gap-7">
|
||||||
<div className="min-h-0 flex-1 max-h-[calc(100vh-180px)] overflow-auto pr-2"><div className="flex flex-col gap-5">
|
<div className="flex flex-col">
|
||||||
{packages.map((licensePackage) => <Card key={licensePackage.name + licensePackage.version} id={licensePackage.name + licensePackage.version}>
|
<p>Last generated: {generatedAt}</p>
|
||||||
<CardHeader><CardTitle className="flex gap-2 items-center"><Badge>{licensePackage.license}</Badge> {licensePackage.name} {licensePackage.version}</CardTitle></CardHeader>
|
<p>Package Count: {packageCount}</p>
|
||||||
{licensePackage.description && <CardContent><CardDescription>{licensePackage.description}</CardDescription></CardContent>}
|
</div>
|
||||||
<CardFooter className="gap-2"><LicenseDialog licensePackage={licensePackage} />
|
<div className="min-h-0 flex-1 max-h-[calc(100vh-180px)] overflow-auto pr-2">
|
||||||
{licensePackage.repository ? <a target="_blank" rel="noreferrer" href={licensePackage.repository.replace("git+", "").replace(".git", "")}><Button variant="outline" className="cursor-pointer">Open Repository</Button></a> : <Button disabled variant="outline" className="cursor-pointer">Open Repository</Button>}
|
<div className="flex flex-col gap-5">
|
||||||
{licensePackage.homepage ? <a target="_blank" rel="noreferrer" href={licensePackage.homepage}><Button variant="outline" className="cursor-pointer">Open Homepage</Button></a> : <Button disabled variant="outline" className="cursor-pointer">Open Homepage</Button>}
|
{packages.map((licensePackage) => (
|
||||||
</CardFooter>
|
<Card
|
||||||
</Card>)}
|
className="pb-0!"
|
||||||
</div></div>
|
key={licensePackage.name + licensePackage.version}
|
||||||
</div>;
|
id={licensePackage.name + licensePackage.version}
|
||||||
|
>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex gap-2 items-center">
|
||||||
|
<Badge>{licensePackage.license}</Badge> {licensePackage.name}{" "}
|
||||||
|
{licensePackage.version}
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
{licensePackage.description && (
|
||||||
|
<CardContent>
|
||||||
|
<CardDescription>
|
||||||
|
{licensePackage.description}
|
||||||
|
</CardDescription>
|
||||||
|
</CardContent>
|
||||||
|
)}
|
||||||
|
<CardFooter className="gap-2">
|
||||||
|
<LicenseDialog licensePackage={licensePackage} />
|
||||||
|
{licensePackage.repository ? (
|
||||||
|
<a
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
href={licensePackage.repository
|
||||||
|
.replace("git+", "")
|
||||||
|
.replace(".git", "")}
|
||||||
|
>
|
||||||
|
<Button variant="outline" className="cursor-pointer">
|
||||||
|
Open Repository
|
||||||
|
</Button>
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
<Button disabled variant="outline" className="cursor-pointer">
|
||||||
|
Open Repository
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{licensePackage.homepage ? (
|
||||||
|
<a
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
href={licensePackage.homepage}
|
||||||
|
>
|
||||||
|
<Button variant="outline" className="cursor-pointer">
|
||||||
|
Open Homepage
|
||||||
|
</Button>
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
<Button disabled variant="outline" className="cursor-pointer">
|
||||||
|
Open Homepage
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function LicenseDialog({ licensePackage }: { licensePackage: (typeof packages)[number] }) {
|
function LicenseDialog({
|
||||||
|
licensePackage,
|
||||||
|
}: {
|
||||||
|
licensePackage: (typeof packages)[number];
|
||||||
|
}) {
|
||||||
const licenseFiles = getLicenseFiles(licensePackage);
|
const licenseFiles = getLicenseFiles(licensePackage);
|
||||||
return <Dialog><DialogTrigger render={<Button disabled={!licenseFiles.some(({ text }) => text)} className="cursor-pointer">Open License</Button>} /><DialogContent className="flex max-h-[85vh] min-h-0 flex-col overflow-hidden sm:max-w-3xl"><div className="min-h-0 flex-1 overflow-y-auto overscroll-contain pr-2">{licenseFiles.map(({ fileName, text }, index) => <section key={fileName} className="border-b last:border-b-0"><h3 className={`border-b pb-2 text-sm font-medium ${index >= 1 && "pt-2"}`}>{fileName}</h3><pre className="pt-2 whitespace-pre-wrap wrap-break-word text-xs leading-relaxed">{text || "License text could not be loaded. Please contact support@tensamin.net"}</pre></section>)}</div></DialogContent></Dialog>;
|
return (
|
||||||
|
<Dialog>
|
||||||
|
<DialogTrigger
|
||||||
|
render={({ onClick }) => (
|
||||||
|
<Button
|
||||||
|
onClick={onClick}
|
||||||
|
disabled={!licenseFiles.some(({ text }) => text)}
|
||||||
|
className="cursor-pointer"
|
||||||
|
>
|
||||||
|
Open License
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<DialogContent className="flex max-h-[85vh] min-h-0 flex-col overflow-hidden sm:max-w-3xl">
|
||||||
|
<div className="min-h-0 flex-1 overflow-y-auto overscroll-contain pr-2">
|
||||||
|
{licenseFiles.map(({ fileName, text }, index) => (
|
||||||
|
<section key={fileName} className="border-b last:border-b-0">
|
||||||
|
<h3
|
||||||
|
className={`border-b pb-2 text-sm font-medium ${index >= 1 && "pt-2"}`}
|
||||||
|
>
|
||||||
|
{fileName}
|
||||||
|
</h3>
|
||||||
|
<pre className="pt-2 whitespace-pre-wrap wrap-break-word text-xs leading-relaxed">
|
||||||
|
{text ||
|
||||||
|
"License text could not be loaded. Please contact support@tensamin.net"}
|
||||||
|
</pre>
|
||||||
|
</section>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,21 +2,40 @@ import MDInput from "@tensamin/markdown/input";
|
||||||
import { useMTP } from "@tensamin/mtp";
|
import { useMTP } from "@tensamin/mtp";
|
||||||
import { mtp } from "@tensamin/shared/data";
|
import { mtp } from "@tensamin/shared/data";
|
||||||
import { useStorage } from "@tensamin/storage/context";
|
import { useStorage } from "@tensamin/storage/context";
|
||||||
import { Avatar, AvatarFallback, AvatarImage, Button, cn, Input, useIsMobile } from "@methanium/ui";
|
import {
|
||||||
|
Avatar,
|
||||||
|
AvatarFallback,
|
||||||
|
AvatarImage,
|
||||||
|
Button,
|
||||||
|
cn,
|
||||||
|
Input,
|
||||||
|
useIsMobile,
|
||||||
|
} from "@methanium/ui";
|
||||||
import { useUser, type User } from "@tensamin/user/context";
|
import { useUser, type User } from "@tensamin/user/context";
|
||||||
import { Check } from "lucide-react";
|
import { Check } from "lucide-react";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
async function prepImage(file: File, size = 300, quality = 0.8): Promise<string> {
|
async function prepImage(
|
||||||
|
file: File,
|
||||||
|
size = 300,
|
||||||
|
quality = 0.8,
|
||||||
|
): Promise<string> {
|
||||||
const bitmap = await createImageBitmap(file);
|
const bitmap = await createImageBitmap(file);
|
||||||
const canvas = document.createElement("canvas");
|
const canvas = document.createElement("canvas");
|
||||||
canvas.width = size; canvas.height = size;
|
canvas.width = size;
|
||||||
|
canvas.height = size;
|
||||||
const context = canvas.getContext("2d");
|
const context = canvas.getContext("2d");
|
||||||
if (!context) throw new Error("Could not get canvas context");
|
if (!context) throw new Error("Could not get canvas context");
|
||||||
const scale = Math.max(size / bitmap.width, size / bitmap.height);
|
const scale = Math.max(size / bitmap.width, size / bitmap.height);
|
||||||
const width = bitmap.width * scale;
|
const width = bitmap.width * scale;
|
||||||
const height = bitmap.height * scale;
|
const height = bitmap.height * scale;
|
||||||
context.drawImage(bitmap, (size - width) / 2, (size - height) / 2, width, height);
|
context.drawImage(
|
||||||
|
bitmap,
|
||||||
|
(size - width) / 2,
|
||||||
|
(size - height) / 2,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
);
|
||||||
return canvas.toDataURL("image/webp", quality);
|
return canvas.toDataURL("image/webp", quality);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -31,14 +50,22 @@ export default function Page() {
|
||||||
const [saveSucceeded, setSaveSucceeded] = useState(false);
|
const [saveSucceeded, setSaveSucceeded] = useState(false);
|
||||||
const avatarUploadRef = useRef<HTMLInputElement>(null);
|
const avatarUploadRef = useRef<HTMLInputElement>(null);
|
||||||
const draftInitializedRef = useRef(false);
|
const draftInitializedRef = useRef(false);
|
||||||
const effectiveAvatar = draftUser.Avatar === "none" ? undefined : draftUser.Avatar;
|
const effectiveAvatar =
|
||||||
const updateDraftUser = (updater: (previous: Partial<User>) => Partial<User>) => {
|
draftUser.Avatar === "none" ? undefined : draftUser.Avatar;
|
||||||
setSaveSucceeded(false); setErrorMessage(""); setDraftUser(updater);
|
const updateDraftUser = (
|
||||||
|
updater: (previous: Partial<User>) => Partial<User>,
|
||||||
|
) => {
|
||||||
|
setSaveSucceeded(false);
|
||||||
|
setErrorMessage("");
|
||||||
|
setDraftUser(updater);
|
||||||
};
|
};
|
||||||
useEffect(() => { void (async () => setCurrentUser(await get(await load("user_id"))))(); }, [get, load]);
|
useEffect(() => {
|
||||||
|
void (async () => setCurrentUser(await get(await load("user_id"))))();
|
||||||
|
}, [get, load]);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!currentUser || draftInitializedRef.current) return;
|
if (!currentUser || draftInitializedRef.current) return;
|
||||||
setDraftUser(currentUser); draftInitializedRef.current = true;
|
setDraftUser(currentUser);
|
||||||
|
draftInitializedRef.current = true;
|
||||||
}, [currentUser]);
|
}, [currentUser]);
|
||||||
async function handleAvatarUpload(file: File) {
|
async function handleAvatarUpload(file: File) {
|
||||||
const avatar = await prepImage(file);
|
const avatar = await prepImage(file);
|
||||||
|
|
@ -46,22 +73,128 @@ export default function Page() {
|
||||||
if (avatarUploadRef.current) avatarUploadRef.current.value = "";
|
if (avatarUploadRef.current) avatarUploadRef.current.value = "";
|
||||||
}
|
}
|
||||||
if (!currentUser) return <p>Loading...</p>;
|
if (!currentUser) return <p>Loading...</p>;
|
||||||
return <>
|
return (
|
||||||
<input ref={avatarUploadRef} hidden onChange={(event) => event.target.files?.[0] && handleAvatarUpload(event.target.files[0])} type="file" />
|
<>
|
||||||
<div className={cn("flex flex-col gap-5", isMobile ? "w-full" : "w-80")}>
|
<input
|
||||||
<div className="flex items-center gap-3"><Avatar className="size-14"><AvatarImage src={effectiveAvatar} /><AvatarFallback className="text-2xl">{draftUser.Display?.slice(0, 2).toUpperCase() || currentUser.Display.slice(0, 2).toUpperCase()}</AvatarFallback></Avatar><div className="flex flex-col gap-1"><p>Avatar</p><div className="flex gap-1"><Button onClick={() => avatarUploadRef.current?.click()}>Upload avatar</Button><Button onClick={() => updateDraftUser((previous) => ({ ...previous, avatar: "none" }))} variant="destructive" disabled={effectiveAvatar === undefined}>Remove</Button></div><p className="text-sm text-muted-foreground">GIFs are supported in decentralised mode or with Tensamin Premium.<br />Maximum file size is 16mb.</p></div></div>
|
ref={avatarUploadRef}
|
||||||
<Input className="w-full" onChange={(event) => updateDraftUser((previous) => ({ ...previous, display: event.target.value }))} placeholder="Display Name" value={draftUser.Display || ""} />
|
hidden
|
||||||
<Input className="w-full" onChange={(event) => updateDraftUser((previous) => ({ ...previous, username: event.target.value }))} placeholder="Username" value={draftUser.Username || ""} />
|
onChange={(event) =>
|
||||||
<MDInput styled paddingY="4px" paddingX="10px" fontSize=".875rem" placeholder="About Me" setValue={(value) => updateDraftUser((previous) => ({ ...previous, about: value }))} value={draftUser.About || ""} />
|
event.target.files?.[0] && handleAvatarUpload(event.target.files[0])
|
||||||
<Button onClick={async () => {
|
}
|
||||||
const { Avatar, ...draftUsersWithoutAvatar } = draftUser;
|
type="file"
|
||||||
const payload = { ...draftUsersWithoutAvatar, ...(typeof Avatar === "string" ? { avatar: Avatar.startsWith("data:") ? (Avatar.split(",", 2)[1] ?? "") : Avatar } : {}) };
|
/>
|
||||||
const validation = mtp.ChangeUserData.request.safeParse(payload);
|
<div className={cn("flex flex-col gap-5", isMobile ? "w-full" : "w-80")}>
|
||||||
if (!validation.success) { setSaveSucceeded(false); setErrorMessage(validation.error.issues[0]?.message ?? "Invalid profile data"); return; }
|
<div className="flex items-center gap-3">
|
||||||
try { await send("ChangeUserData", validation.data); setSaveSucceeded(true); setErrorMessage(""); }
|
<Avatar className="size-14">
|
||||||
catch (error) { setSaveSucceeded(false); setErrorMessage("Failed to update profile: " + error); }
|
<AvatarImage src={effectiveAvatar} />
|
||||||
}}>{saveSucceeded ? <span className="inline-flex items-center gap-1.5"><Check className="size-4" />Saved</span> : "Save"}</Button>
|
<AvatarFallback className="text-2xl">
|
||||||
{errorMessage && <p className="text-sm text-destructive">{errorMessage}</p>}
|
{draftUser.Display?.slice(0, 2).toUpperCase() ||
|
||||||
</div>
|
currentUser.Display.slice(0, 2).toUpperCase()}
|
||||||
</>;
|
</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<p>Avatar</p>
|
||||||
|
<div className="flex gap-1">
|
||||||
|
<Button onClick={() => avatarUploadRef.current?.click()}>
|
||||||
|
Upload avatar
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() =>
|
||||||
|
updateDraftUser((previous) => ({
|
||||||
|
...previous,
|
||||||
|
avatar: "none",
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
variant="destructive"
|
||||||
|
disabled={effectiveAvatar === undefined}
|
||||||
|
>
|
||||||
|
Remove
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
GIFs are supported in decentralised mode or with Tensamin Premium.
|
||||||
|
<br />
|
||||||
|
Maximum file size is 16mb.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
className="w-full"
|
||||||
|
onChange={(event) =>
|
||||||
|
updateDraftUser((previous) => ({
|
||||||
|
...previous,
|
||||||
|
display: event.target.value,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
placeholder="Display Name"
|
||||||
|
value={draftUser.Display || ""}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
className="w-full"
|
||||||
|
onChange={(event) =>
|
||||||
|
updateDraftUser((previous) => ({
|
||||||
|
...previous,
|
||||||
|
username: event.target.value,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
placeholder="Username"
|
||||||
|
value={draftUser.Username || ""}
|
||||||
|
/>
|
||||||
|
<MDInput
|
||||||
|
styled
|
||||||
|
paddingY="4px"
|
||||||
|
paddingX="10px"
|
||||||
|
fontSize=".875rem"
|
||||||
|
placeholder="About Me"
|
||||||
|
setValue={(value) =>
|
||||||
|
updateDraftUser((previous) => ({ ...previous, about: value }))
|
||||||
|
}
|
||||||
|
value={draftUser.About || ""}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
onClick={async () => {
|
||||||
|
const { Avatar, ...draftUsersWithoutAvatar } = draftUser;
|
||||||
|
const payload = {
|
||||||
|
...draftUsersWithoutAvatar,
|
||||||
|
...(typeof Avatar === "string"
|
||||||
|
? {
|
||||||
|
avatar: Avatar.startsWith("data:")
|
||||||
|
? (Avatar.split(",", 2)[1] ?? "")
|
||||||
|
: Avatar,
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
};
|
||||||
|
const validation = mtp.ChangeUserData.request.safeParse(payload);
|
||||||
|
if (!validation.success) {
|
||||||
|
setSaveSucceeded(false);
|
||||||
|
setErrorMessage(
|
||||||
|
validation.error.issues[0]?.message ?? "Invalid profile data",
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await send("ChangeUserData", validation.data);
|
||||||
|
setSaveSucceeded(true);
|
||||||
|
setErrorMessage("");
|
||||||
|
} catch (error) {
|
||||||
|
setSaveSucceeded(false);
|
||||||
|
setErrorMessage("Failed to update profile: " + error);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{saveSucceeded ? (
|
||||||
|
<span className="inline-flex items-center gap-1.5">
|
||||||
|
<Check className="size-4" />
|
||||||
|
Saved
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
"Save"
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
{errorMessage && (
|
||||||
|
<p className="text-sm text-destructive">{errorMessage}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,18 +8,87 @@ export default function Page() {
|
||||||
const [currentOmegaUrl, setCurrentOmegaUrl] = useState("");
|
const [currentOmegaUrl, setCurrentOmegaUrl] = useState("");
|
||||||
const [draftForcedOmikronUrl, setDraftForcedOmikronUrl] = useState("");
|
const [draftForcedOmikronUrl, setDraftForcedOmikronUrl] = useState("");
|
||||||
const [currentForcedOmikronUrl, setCurrentForcedOmikronUrl] = useState("");
|
const [currentForcedOmikronUrl, setCurrentForcedOmikronUrl] = useState("");
|
||||||
const [draftForcedOmikronPublicKey, setDraftForcedOmikronPublicKey] = useState("");
|
const [draftForcedOmikronPublicKey, setDraftForcedOmikronPublicKey] =
|
||||||
const [currentForcedOmikronPublicKey, setCurrentForcedOmikronPublicKey] = useState("");
|
useState("");
|
||||||
|
const [currentForcedOmikronPublicKey, setCurrentForcedOmikronPublicKey] =
|
||||||
|
useState("");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
load("omega_url").then((value) => { setDraftOmegaUrl(value); setCurrentOmegaUrl(value); });
|
load("omega_url").then((value) => {
|
||||||
load("forced_omikron_url").then((value) => { setDraftForcedOmikronUrl(value || ""); setCurrentForcedOmikronUrl(value || ""); });
|
setDraftOmegaUrl(value);
|
||||||
load("forced_omikron_public_key").then((value) => { setDraftForcedOmikronPublicKey(value || ""); setCurrentForcedOmikronPublicKey(value || ""); });
|
setCurrentOmegaUrl(value);
|
||||||
|
});
|
||||||
|
load("forced_omikron_url").then((value) => {
|
||||||
|
setDraftForcedOmikronUrl(value || "");
|
||||||
|
setCurrentForcedOmikronUrl(value || "");
|
||||||
|
});
|
||||||
|
load("forced_omikron_public_key").then((value) => {
|
||||||
|
setDraftForcedOmikronPublicKey(value || "");
|
||||||
|
setCurrentForcedOmikronPublicKey(value || "");
|
||||||
|
});
|
||||||
}, [load]);
|
}, [load]);
|
||||||
|
|
||||||
return <div className="flex flex-col gap-8">
|
return (
|
||||||
<p className="text-destructive">It's best not to touch these settings! They can be exploited to gain access to your account!</p>
|
<div className="flex flex-col gap-8">
|
||||||
<div className="flex flex-col gap-2"><Label>Omega Url</Label><div className="flex gap-1"><Input value={draftOmegaUrl} onChange={(event) => setDraftOmegaUrl(event.target.value)} /><Button disabled={currentOmegaUrl === draftOmegaUrl} onClick={() => save("omega_url", draftOmegaUrl).then(() => setCurrentOmegaUrl(draftOmegaUrl))}>Save</Button></div></div>
|
<p className="text-destructive">
|
||||||
<div className="flex flex-col gap-2"><Label>Forced Omikron</Label><div className="flex gap-1"><Input placeholder="URL..." value={draftForcedOmikronUrl} onChange={(event) => setDraftForcedOmikronUrl(event.target.value)} /><Input placeholder="Public Key..." value={draftForcedOmikronPublicKey} onChange={(event) => setDraftForcedOmikronPublicKey(event.target.value)} /><Button disabled={currentForcedOmikronUrl === draftForcedOmikronUrl && currentForcedOmikronPublicKey === draftForcedOmikronPublicKey} onClick={() => { save("forced_omikron_url", draftForcedOmikronUrl).then(() => setCurrentForcedOmikronUrl(draftForcedOmikronUrl)); save("forced_omikron_public_key", draftForcedOmikronPublicKey).then(() => setCurrentForcedOmikronPublicKey(draftForcedOmikronPublicKey)); }}>Save</Button></div></div>
|
It's best not to touch these settings! They can be exploited to gain
|
||||||
</div>;
|
access to your account!
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Label>Omega Url</Label>
|
||||||
|
<div className="flex gap-1">
|
||||||
|
<Input
|
||||||
|
value={draftOmegaUrl}
|
||||||
|
onChange={(event) => setDraftOmegaUrl(event.target.value)}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
disabled={currentOmegaUrl === draftOmegaUrl}
|
||||||
|
onClick={() =>
|
||||||
|
save("omega_url", draftOmegaUrl).then(() =>
|
||||||
|
setCurrentOmegaUrl(draftOmegaUrl),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Label>Forced Omikron</Label>
|
||||||
|
<div className="flex gap-1">
|
||||||
|
<Input
|
||||||
|
placeholder="URL..."
|
||||||
|
value={draftForcedOmikronUrl}
|
||||||
|
onChange={(event) => setDraftForcedOmikronUrl(event.target.value)}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
placeholder="Public Key..."
|
||||||
|
value={draftForcedOmikronPublicKey}
|
||||||
|
onChange={(event) =>
|
||||||
|
setDraftForcedOmikronPublicKey(event.target.value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
disabled={
|
||||||
|
currentForcedOmikronUrl === draftForcedOmikronUrl &&
|
||||||
|
currentForcedOmikronPublicKey === draftForcedOmikronPublicKey
|
||||||
|
}
|
||||||
|
onClick={() => {
|
||||||
|
save("forced_omikron_url", draftForcedOmikronUrl).then(() =>
|
||||||
|
setCurrentForcedOmikronUrl(draftForcedOmikronUrl),
|
||||||
|
);
|
||||||
|
save(
|
||||||
|
"forced_omikron_public_key",
|
||||||
|
draftForcedOmikronPublicKey,
|
||||||
|
).then(() =>
|
||||||
|
setCurrentForcedOmikronPublicKey(draftForcedOmikronPublicKey),
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,18 @@
|
||||||
import { StylePicker } from "@methanium/ui";
|
import { ThemeCustomizer } from "@methanium/ui";
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
return <div className="overflow-y-auto"><StylePicker /><div className="absolute bottom-0 right-0 mb-3 mr-2"><a className="block w-60 text-xs whitespace-pre-wrap" href="https://git.methanium.net/tensamin/client/issues/new" target="_blank" rel="noreferrer">Please open a Git issue to help us improve this feature. We want to get it right.</a></div></div>;
|
return (
|
||||||
|
<div className="h-full overflow-y-auto p-5 md:p-8">
|
||||||
|
<ThemeCustomizer />
|
||||||
|
<a
|
||||||
|
className="mt-8 block max-w-md text-xs text-muted-foreground"
|
||||||
|
href="https://git.methanium.net/tensamin/client/issues/new"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
Please open an issue on Git to tell us what you would like to see in
|
||||||
|
here.
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import type { ThemeDesign } from "@methanium/ui";
|
||||||
|
|
||||||
import { legalDocsSchema } from "./features/legal/schema";
|
import { legalDocsSchema } from "./features/legal/schema";
|
||||||
import {
|
import {
|
||||||
|
|
@ -422,6 +423,7 @@ export interface Storage extends SettingsStorageDefaults {
|
||||||
user_id: number;
|
user_id: number;
|
||||||
mtp_keyring: string;
|
mtp_keyring: string;
|
||||||
onboarding_done: boolean;
|
onboarding_done: boolean;
|
||||||
|
onboarding_started: boolean;
|
||||||
ppandtos_done: boolean;
|
ppandtos_done: boolean;
|
||||||
accepted_terms_of_service: boolean;
|
accepted_terms_of_service: boolean;
|
||||||
accepted_privacy_policy: boolean;
|
accepted_privacy_policy: boolean;
|
||||||
|
|
@ -445,6 +447,8 @@ export interface Storage extends SettingsStorageDefaults {
|
||||||
theme_tint: "soft" | "hard" | "extreme";
|
theme_tint: "soft" | "hard" | "extreme";
|
||||||
theme_border_radius: number;
|
theme_border_radius: number;
|
||||||
theme_custom_css: string;
|
theme_custom_css: string;
|
||||||
|
theme_parent: string;
|
||||||
|
theme_design: ThemeDesign;
|
||||||
chat_trusted_domains: string[];
|
chat_trusted_domains: string[];
|
||||||
chat_picker_saved_media: string[];
|
chat_picker_saved_media: string[];
|
||||||
chat_picker_last_tab: "gif" | "meme" | "saved";
|
chat_picker_last_tab: "gif" | "meme" | "saved";
|
||||||
|
|
@ -460,6 +464,7 @@ export const storageDefaults: Storage = {
|
||||||
user_id: 0,
|
user_id: 0,
|
||||||
mtp_keyring: "",
|
mtp_keyring: "",
|
||||||
onboarding_done: false,
|
onboarding_done: false,
|
||||||
|
onboarding_started: false,
|
||||||
ppandtos_done: false,
|
ppandtos_done: false,
|
||||||
accepted_terms_of_service: false,
|
accepted_terms_of_service: false,
|
||||||
accepted_privacy_policy: false,
|
accepted_privacy_policy: false,
|
||||||
|
|
@ -498,8 +503,18 @@ export const storageDefaults: Storage = {
|
||||||
theme_primary_color: "",
|
theme_primary_color: "",
|
||||||
theme_polarity: "system",
|
theme_polarity: "system",
|
||||||
theme_tint: "soft",
|
theme_tint: "soft",
|
||||||
theme_border_radius: 0.55,
|
theme_border_radius: 0.5,
|
||||||
theme_custom_css: "",
|
theme_custom_css: "",
|
||||||
|
theme_parent: "tensamin",
|
||||||
|
theme_design: {
|
||||||
|
density: 1,
|
||||||
|
borderWidth: 1,
|
||||||
|
shadowStrength: 0,
|
||||||
|
fontScale: 1,
|
||||||
|
headingWeight: 600,
|
||||||
|
motion: 1,
|
||||||
|
fontFamily: "public-sans",
|
||||||
|
},
|
||||||
chat_trusted_domains: [
|
chat_trusted_domains: [
|
||||||
// Other platforms
|
// Other platforms
|
||||||
"cdn.discordapp.com",
|
"cdn.discordapp.com",
|
||||||
|
|
|
||||||
462
pnpm-lock.yaml
generated
462
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
|
|
@ -6,5 +6,5 @@ allowBuilds:
|
||||||
electron-winstaller: true
|
electron-winstaller: true
|
||||||
esbuild: true
|
esbuild: true
|
||||||
overrides:
|
overrides:
|
||||||
"@methanium/ui": "https://git.methanium.net/methanium/ui/releases/download/0.0.1/methanium-ui.tgz"
|
"@methanium/ui": "https://git.methanium.net/methanium/ui/releases/download/0.0.6/methanium-ui.tgz"
|
||||||
mtp: "https://git.methanium.net/methanium/mtp/releases/download/0.2.0-3919eb46fd/mtp-0.2.0.tgz"
|
mtp: "https://git.methanium.net/methanium/mtp/releases/download/0.2.0-3919eb46fd/mtp-0.2.0.tgz"
|
||||||
|
|
|
||||||
270
type-maps.yaml
270
type-maps.yaml
|
|
@ -1,270 +0,0 @@
|
||||||
protocol_version: "1.0"
|
|
||||||
|
|
||||||
# Note that markers 0 to 31 are reserved for default use, manually working with them is not recommended
|
|
||||||
|
|
||||||
# Fixed CommunicationType markers are:
|
|
||||||
# Error: 0
|
|
||||||
# ErrorParsing: 1
|
|
||||||
# ErrorBadVersion: 2
|
|
||||||
# Disconnect: 3
|
|
||||||
# Redirect: 4
|
|
||||||
# Shutdown: 5
|
|
||||||
# BadRequest: 6
|
|
||||||
# Unauthorized: 7
|
|
||||||
# Forbidden: 8
|
|
||||||
# NotFound: 9
|
|
||||||
# TooManyRequests: 10
|
|
||||||
# InternalServerError: 11
|
|
||||||
# BadGateway: 12
|
|
||||||
# ServiceUnavailable: 13
|
|
||||||
# GatewayTimeout: 14
|
|
||||||
# Identification: 15
|
|
||||||
# IdentificationResponse: 16
|
|
||||||
# Register: 17
|
|
||||||
# RegisterResponse: 18
|
|
||||||
# Ping: 19
|
|
||||||
# Pong: 20
|
|
||||||
|
|
||||||
# Fixed Data Type markers are:
|
|
||||||
# Error: 0
|
|
||||||
# ErrorParsing: 1
|
|
||||||
# ErrorMessage: 2
|
|
||||||
# Version: 3
|
|
||||||
# Description: 4
|
|
||||||
# Timestamp: 5
|
|
||||||
# Id: 6
|
|
||||||
# ClientNonce: 7
|
|
||||||
# ServerNonce: 8
|
|
||||||
# PublicKeys: 9
|
|
||||||
# Signature: 10
|
|
||||||
# Connected: 11
|
|
||||||
|
|
||||||
type_maps:
|
|
||||||
"1.0":
|
|
||||||
CommunicationTypes:
|
|
||||||
ErrorProtocol: 33
|
|
||||||
ErrorAnonymous: 34
|
|
||||||
ErrorInternal: 35
|
|
||||||
ErrorInvalidData: 36
|
|
||||||
ErrorInvalidUserId: 37
|
|
||||||
ErrorInvalidOmikronId: 38
|
|
||||||
ErrorNotFound: 39
|
|
||||||
ErrorNotAuthenticated: 40
|
|
||||||
ErrorNoIota: 41
|
|
||||||
ErrorInvalidChallenge: 42
|
|
||||||
ErrorInvalidSecret: 43
|
|
||||||
ErrorInvalidPrivateKey: 44
|
|
||||||
ErrorInvalidPublicKey: 45
|
|
||||||
ErrorNoUserId: 46
|
|
||||||
ErrorNoCallId: 47
|
|
||||||
ErrorInvalidCallId: 48
|
|
||||||
Success: 49
|
|
||||||
ShortenLink: 50
|
|
||||||
SettingsSave: 51
|
|
||||||
SettingsLoad: 52
|
|
||||||
SettingsList: 53
|
|
||||||
GlobalSettingsSave: 54
|
|
||||||
GlobalSettingsLoad: 55
|
|
||||||
Message: 56
|
|
||||||
MessageState: 57
|
|
||||||
MessageSend: 58
|
|
||||||
MessageLive: 59
|
|
||||||
MessageOtherIota: 60
|
|
||||||
MessageChunk: 61
|
|
||||||
MessageGet: 143
|
|
||||||
MessagesGet: 62
|
|
||||||
MessageDelete: 149
|
|
||||||
PushNotification: 63
|
|
||||||
ReadNotification: 64
|
|
||||||
GetNotifications: 65
|
|
||||||
TauriIdentification: 66
|
|
||||||
ChangeConfirm: 67
|
|
||||||
ConfirmReceive: 68
|
|
||||||
ConfirmRead: 69
|
|
||||||
GetChats: 70
|
|
||||||
GetStates: 71
|
|
||||||
AddCommunity: 72
|
|
||||||
RemoveCommunity: 73
|
|
||||||
GetCommunities: 74
|
|
||||||
RegisterIota: 81
|
|
||||||
RegisterIotaSuccess: 82
|
|
||||||
AddConversation: 85
|
|
||||||
SendChat: 86
|
|
||||||
ClientChanged: 87
|
|
||||||
ClientConnected: 88
|
|
||||||
ClientDisconnected: 89
|
|
||||||
ClientClosed: 90
|
|
||||||
PublicKey: 91
|
|
||||||
PrivateKey: 92
|
|
||||||
WebrtcSdp: 93
|
|
||||||
WebrtcIce: 94
|
|
||||||
StartStream: 95
|
|
||||||
EndStream: 96
|
|
||||||
WatchStream: 97
|
|
||||||
CallToken: 98
|
|
||||||
CallInvite: 99
|
|
||||||
CallDisconnectUser: 100
|
|
||||||
CallTimeoutUser: 101
|
|
||||||
CallSetAnonymousJoining: 102
|
|
||||||
CallData: 103
|
|
||||||
EndCall: 104
|
|
||||||
Function: 105
|
|
||||||
Update: 106
|
|
||||||
CreateUser: 107
|
|
||||||
RhoUpdate: 108
|
|
||||||
UserConnected: 109
|
|
||||||
UserDisconnected: 110
|
|
||||||
IotaConnected: 111
|
|
||||||
IotaDisconnected: 112
|
|
||||||
SyncClientIotaStatus: 113
|
|
||||||
GetUserData: 114
|
|
||||||
GetIotaData: 115
|
|
||||||
IotaUserData: 116
|
|
||||||
ChangeUserData: 117
|
|
||||||
ChangeIotaData: 118
|
|
||||||
GetRegister: 119
|
|
||||||
CompleteRegisterUser: 120
|
|
||||||
CompleteRegisterIota: 121
|
|
||||||
DeleteUser: 122
|
|
||||||
DeleteIota: 123
|
|
||||||
StartRegister: 124
|
|
||||||
CompleteRegister: 125
|
|
||||||
GetApp: 126
|
|
||||||
CreateApp: 127
|
|
||||||
DeleteApp: 128
|
|
||||||
SaveAppData: 129
|
|
||||||
LoadAppData: 130
|
|
||||||
AppIdentification: 131
|
|
||||||
AppChallenge: 132
|
|
||||||
AppChallengeResponse: 133
|
|
||||||
AppIdentificationResponse: 134
|
|
||||||
LoadTxtRecord: 135
|
|
||||||
ErrorNotSet: 136
|
|
||||||
SetChatSecret: 139
|
|
||||||
GetChatSecret: 140
|
|
||||||
ChatSecretResponse: 141
|
|
||||||
ChatSecretForward: 142
|
|
||||||
MessageEditLive: 144
|
|
||||||
MessageEdit: 145
|
|
||||||
MessageReactionAdd: 146
|
|
||||||
MessageReactionRemove: 147
|
|
||||||
MessageReactionLive: 148
|
|
||||||
MessageDeleteLive: 150
|
|
||||||
DataTypes:
|
|
||||||
ErrorType: 32
|
|
||||||
ErrorProtocol: 33
|
|
||||||
AcceptedIds: 34
|
|
||||||
Uuid: 35
|
|
||||||
RegisterId: 36
|
|
||||||
Link: 37
|
|
||||||
Settings: 38
|
|
||||||
SettingsName: 39
|
|
||||||
ChatPartnerId: 40
|
|
||||||
ChatPartnerName: 41
|
|
||||||
IotaId: 42
|
|
||||||
UserId: 43
|
|
||||||
UserIds: 44
|
|
||||||
IotaIds: 45
|
|
||||||
UserState: 46
|
|
||||||
UserStates: 47
|
|
||||||
UserPings: 48
|
|
||||||
CallState: 49
|
|
||||||
ScreenShare: 50
|
|
||||||
PrivateKeyHash: 51
|
|
||||||
#Accepted: 52
|
|
||||||
AcceptedProfiles: 53
|
|
||||||
DeniedProfiles: 54
|
|
||||||
Content: 55
|
|
||||||
Messages: 56
|
|
||||||
Notifications: 57
|
|
||||||
SendTime: 58
|
|
||||||
GetTime: 59
|
|
||||||
GetVariant: 60
|
|
||||||
SharedSecretOwn: 61
|
|
||||||
SharedSecretOther: 62
|
|
||||||
SharedSecretSign: 63
|
|
||||||
SharedSecret: 64
|
|
||||||
CallId: 65
|
|
||||||
CallToken: 66
|
|
||||||
CallSecret: 67
|
|
||||||
Untill: 68
|
|
||||||
Enabled: 69
|
|
||||||
StartDate: 70
|
|
||||||
EndDate: 71
|
|
||||||
ReceiverId: 72
|
|
||||||
SenderId: 73
|
|
||||||
Signed: 75
|
|
||||||
Message: 76
|
|
||||||
MessageState: 77
|
|
||||||
LastPing: 78
|
|
||||||
PingIota: 79
|
|
||||||
PingClients: 80
|
|
||||||
Matches: 81
|
|
||||||
Omikron: 82
|
|
||||||
Offset: 83
|
|
||||||
Amount: 84
|
|
||||||
Position: 85
|
|
||||||
Name: 86
|
|
||||||
Path: 87
|
|
||||||
Codec: 88
|
|
||||||
Function: 89
|
|
||||||
Payload: 90
|
|
||||||
Result: 91
|
|
||||||
Interactables: 92
|
|
||||||
WantToWatch: 93
|
|
||||||
Watcher: 94
|
|
||||||
CreatedAt: 95
|
|
||||||
Username: 96
|
|
||||||
Display: 97
|
|
||||||
Avatar: 98
|
|
||||||
About: 99
|
|
||||||
Status: 100
|
|
||||||
PublicKey: 101
|
|
||||||
SubLevel: 102
|
|
||||||
SubEnd: 103
|
|
||||||
CommunityAddress: 104
|
|
||||||
CommunityTitle: 106
|
|
||||||
Communities: 107
|
|
||||||
RhoConnections: 108
|
|
||||||
User: 109
|
|
||||||
OnlineStatus: 110
|
|
||||||
OmikronId: 111
|
|
||||||
OmikronConnections: 112
|
|
||||||
ResetToken: 113
|
|
||||||
NewToken: 114
|
|
||||||
CallInvited: 115
|
|
||||||
CallMembers: 116
|
|
||||||
Calls: 117
|
|
||||||
Timeout: 118
|
|
||||||
HasAdmin: 119
|
|
||||||
LastMessageAt: 120
|
|
||||||
Height: 121
|
|
||||||
SentBySelf: 122
|
|
||||||
SessionId: 123
|
|
||||||
Contacts: 124
|
|
||||||
LastMessage: 125
|
|
||||||
AppIdentifier: 127
|
|
||||||
AppPrivateKey: 128
|
|
||||||
AppPublicKey: 129
|
|
||||||
AppSession: 130
|
|
||||||
AppData: 131
|
|
||||||
TauriToken: 132
|
|
||||||
Challenge: 133
|
|
||||||
EncryptedPayload: 134
|
|
||||||
SecurePayload: 135
|
|
||||||
DeviceId: 136
|
|
||||||
ClientId: 137
|
|
||||||
SecretId: 142
|
|
||||||
VersionNumber: 143
|
|
||||||
EncryptedSecret: 144
|
|
||||||
WrappingScheme: 146
|
|
||||||
UpdatedAt: 147
|
|
||||||
ChatId: 148
|
|
||||||
KemCiphertext: 149
|
|
||||||
SenderUserId: 152
|
|
||||||
RecipientUserId: 153
|
|
||||||
Recipients: 154
|
|
||||||
Edited: 155
|
|
||||||
Reactions: 156
|
|
||||||
Reaction: 157
|
|
||||||
ReplyId: 158
|
|
||||||
Loading…
Reference in a new issue