Updated icons, new identification
|
|
@ -4,4 +4,5 @@ coverage
|
||||||
.tmp
|
.tmp
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
bun.lock
|
bun.lock
|
||||||
apps/desktop/build
|
apps/desktop/build
|
||||||
|
apps/mobile/src-tauri
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import type { ElectrobunConfig } from "electrobun";
|
import type { ElectrobunConfig } from "electrobun";
|
||||||
|
|
||||||
import { version } from "@tensamin/shared/package.json";
|
import { version } from "../../package.json";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
app: {
|
app: {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "@tensamin/mobile",
|
"name": "@tensamin/mobile",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.1.0",
|
"version": "0.0.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": {
|
"exports": {
|
||||||
"./context": "./src/context.tsx"
|
"./context": "./src/context.tsx"
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||||
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
|
</adaptive-icon>
|
||||||
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 4 KiB |
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 8.9 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 7.8 KiB After Width: | Height: | Size: 8.8 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 7.8 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 26 KiB |
|
|
@ -0,0 +1,4 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="ic_launcher_background">#fff</color>
|
||||||
|
</resources>
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"$schema": "https://schema.tauri.app/config/2",
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"productName": "tensamin",
|
"productName": "tensamin",
|
||||||
"version": "0.1.0",
|
"version": "../../../package.json",
|
||||||
"identifier": "net.tensamin.client",
|
"identifier": "net.tensamin.client",
|
||||||
"build": {
|
"build": {
|
||||||
"beforeDevCommand": "cd ../web && bun run dev && cd -",
|
"beforeDevCommand": "cd ../web && bun run dev && cd -",
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ import { useLocation, useNavigate, useSearch } from "@tanstack/react-router";
|
||||||
import { useCall } from "@tensamin/call/context";
|
import { useCall } from "@tensamin/call/context";
|
||||||
import Wrapper from "@tensamin/user/wrapper";
|
import Wrapper from "@tensamin/user/wrapper";
|
||||||
import { Skeleton } from "@tensamin/ui/cmp/skeleton";
|
import { Skeleton } from "@tensamin/ui/cmp/skeleton";
|
||||||
import { useConversation } from "@/features/conversation/context";
|
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
|
|
@ -14,16 +13,17 @@ import {
|
||||||
import { displayCallId } from "@tensamin/call/utils";
|
import { displayCallId } from "@tensamin/call/utils";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { SidebarTrigger, useSidebar } from "@tensamin/ui/cmp/sidebar";
|
import { SidebarTrigger, useSidebar } from "@tensamin/ui/cmp/sidebar";
|
||||||
|
import { useTTP } from "@tensamin/ttp/context";
|
||||||
|
|
||||||
export default function Navbar({ forMobile }: { forMobile: boolean }) {
|
export default function Navbar({ forMobile }: { forMobile: boolean }) {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { joinCall, state } = useCall();
|
const { joinCall, state } = useCall();
|
||||||
const { conversations } = useConversation();
|
const { contacts } = useTTP();
|
||||||
|
|
||||||
const { pathname } = useLocation();
|
const { pathname } = useLocation();
|
||||||
const { id } = useSearch({ strict: false });
|
const { id } = useSearch({ strict: false });
|
||||||
|
|
||||||
const currentCalls = id ? conversations?.[id]?.calls || [] : [];
|
const currentCalls = id ? contacts?.[id]?.calls || [] : [];
|
||||||
//const currentCalls = ["leck", "schleck", "und", "eck"];
|
//const currentCalls = ["leck", "schleck", "und", "eck"];
|
||||||
|
|
||||||
const [selectOpen, setSelectOpen] = useState(false);
|
const [selectOpen, setSelectOpen] = useState(false);
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ import { Input } from "@tensamin/ui/cmp/input";
|
||||||
import { Label } from "@tensamin/ui/cmp/label";
|
import { Label } from "@tensamin/ui/cmp/label";
|
||||||
import { useStorage } from "@tensamin/storage/context";
|
import { useStorage } from "@tensamin/storage/context";
|
||||||
import { log, toast } from "@tensamin/shared/log";
|
import { log, toast } from "@tensamin/shared/log";
|
||||||
import { useNavigate } from "@tanstack/react-router";
|
|
||||||
import { Upload } from "lucide-react";
|
import { Upload } from "lucide-react";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
@ -55,7 +54,6 @@ function parseTuFileContent(rawFileContent: string): {
|
||||||
export default function Form() {
|
export default function Form() {
|
||||||
const uploadRef = React.useRef<HTMLInputElement | null>(null);
|
const uploadRef = React.useRef<HTMLInputElement | null>(null);
|
||||||
const { save } = useStorage();
|
const { save } = useStorage();
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles uploaded .tu files and stores resolved credentials.
|
* Handles uploaded .tu files and stores resolved credentials.
|
||||||
|
|
@ -73,16 +71,17 @@ export default function Form() {
|
||||||
const raw = await file.text();
|
const raw = await file.text();
|
||||||
const parsed = parseTuFileContent(raw);
|
const parsed = parseTuFileContent(raw);
|
||||||
|
|
||||||
|
await save("session_id", Date.now());
|
||||||
await save("user_id", parsed.userId);
|
await save("user_id", parsed.userId);
|
||||||
await save("private_key", parsed.privateKey);
|
await save("private_key", parsed.privateKey);
|
||||||
|
|
||||||
void navigate({ to: "/" });
|
location.href = "/";
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log(0, "Login", "red", error);
|
log(0, "Login", "red", error);
|
||||||
toast("error", "Failed to load file");
|
toast("error", "Failed to load file");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[navigate, save],
|
[save],
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -123,16 +122,17 @@ export default function Form() {
|
||||||
|
|
||||||
const user = parse.data;
|
const user = parse.data;
|
||||||
|
|
||||||
|
await save("session_id", Date.now());
|
||||||
await save("user_id", user.user_id);
|
await save("user_id", user.user_id);
|
||||||
await save("private_key", inputParse.data.private_key);
|
await save("private_key", inputParse.data.private_key);
|
||||||
|
|
||||||
navigate({ to: "/" });
|
location.href = "/";
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log(0, "Login", "red", error);
|
log(0, "Login", "red", error);
|
||||||
toast("error", "Failed to fetch user data");
|
toast("error", "Failed to fetch user data");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[navigate, save],
|
[save],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -1,102 +0,0 @@
|
||||||
import { useTTP } from "@tensamin/ttp/context";
|
|
||||||
import {
|
|
||||||
createContext,
|
|
||||||
useContext,
|
|
||||||
useEffect,
|
|
||||||
useState,
|
|
||||||
type ReactNode,
|
|
||||||
} from "react";
|
|
||||||
|
|
||||||
import { toast } from "@tensamin/shared/log";
|
|
||||||
import type {
|
|
||||||
Community,
|
|
||||||
Conversation,
|
|
||||||
} from "@tensamin/shared/features/conversation/schema";
|
|
||||||
|
|
||||||
interface contextValue {
|
|
||||||
conversations: Conversation[] | null;
|
|
||||||
communities: Community[] | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ConversationContext = createContext<contextValue | undefined>(undefined);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Executes ConversationProvider.
|
|
||||||
* @param props Parameter props.
|
|
||||||
* @returns unknown.
|
|
||||||
*/
|
|
||||||
export default function ConversationProvider(props: { children: ReactNode }) {
|
|
||||||
const [conversations, setConversations] = useState<Conversation[] | null>(
|
|
||||||
null,
|
|
||||||
);
|
|
||||||
const [communities, setCommunities] = useState<Community[] | null>(null);
|
|
||||||
|
|
||||||
const { send } = useTTP();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
let active = true;
|
|
||||||
|
|
||||||
send("get_conversations", {})
|
|
||||||
.then((data) => {
|
|
||||||
if (active) {
|
|
||||||
setConversations(data.data.user_ids);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
setConversations([
|
|
||||||
{
|
|
||||||
last_message_at: 0,
|
|
||||||
user_id: 0,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
toast("error", "Failed to load conversations");
|
|
||||||
});
|
|
||||||
|
|
||||||
send("get_communities", {})
|
|
||||||
.then((data) => {
|
|
||||||
if (active) {
|
|
||||||
setCommunities(data.data.communities);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
setCommunities([
|
|
||||||
{
|
|
||||||
community_address: "none",
|
|
||||||
community_title: "Failed",
|
|
||||||
position: "",
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
toast("error", "Failed to load communities");
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
active = false;
|
|
||||||
};
|
|
||||||
}, [send]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ConversationContext.Provider
|
|
||||||
value={{
|
|
||||||
conversations,
|
|
||||||
communities,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{props.children}
|
|
||||||
</ConversationContext.Provider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Executes useConversation.
|
|
||||||
* @param none This function has no parameters.
|
|
||||||
* @returns contextValue.
|
|
||||||
*/
|
|
||||||
export function useConversation(): contextValue {
|
|
||||||
const context = useContext(ConversationContext);
|
|
||||||
if (!context) {
|
|
||||||
throw new Error(
|
|
||||||
"useConversation must be used within a ConversationProvider",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return context;
|
|
||||||
}
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { useVirtualizer } from "@tanstack/react-virtual";
|
import { useVirtualizer } from "@tanstack/react-virtual";
|
||||||
import { useConversation } from "../context";
|
import { useTTP } from "@tensamin/ttp/context";
|
||||||
|
|
||||||
import Switch from "./switch";
|
import Switch from "./switch";
|
||||||
import ConversationModal from "../modal/conversation";
|
import ConversationModal from "../modal/conversation";
|
||||||
|
|
@ -17,8 +17,8 @@ export default function List() {
|
||||||
"conversations" | "communities"
|
"conversations" | "communities"
|
||||||
>("conversations");
|
>("conversations");
|
||||||
|
|
||||||
const { conversations, communities } = useConversation();
|
const { contacts, communities } = useTTP();
|
||||||
const items = category === "conversations" ? conversations : communities;
|
const items = category === "conversations" ? contacts : communities;
|
||||||
|
|
||||||
const scrollRef = React.useRef<HTMLDivElement | null>(null);
|
const scrollRef = React.useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
|
|
@ -66,14 +66,14 @@ export default function List() {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{category === "conversations" ? (
|
{category === "conversations" ? (
|
||||||
conversations?.[virtualItem.index] ? (
|
contacts?.[virtualItem.index] ? (
|
||||||
<ConversationModal
|
<ConversationModal
|
||||||
userId={conversations[virtualItem.index].user_id}
|
userId={contacts[virtualItem.index].user_id}
|
||||||
/>
|
/>
|
||||||
) : null
|
) : null
|
||||||
) : communities?.[virtualItem.index] ? (
|
) : communities?.[virtualItem.index] ? (
|
||||||
<CommunityModal
|
<CommunityModal
|
||||||
community={communities[virtualItem.index]}
|
//community={communities[virtualItem.index]}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,3 @@
|
||||||
import type { Community } from "@tensamin/shared/features/conversation/schema";
|
export default function CommunityModal() {
|
||||||
|
return <div>Not implemented</div>;
|
||||||
/**
|
|
||||||
* Executes CommunityModal.
|
|
||||||
* @param props Parameter props.
|
|
||||||
* @returns unknown.
|
|
||||||
*/
|
|
||||||
export default function CommunityModal(props: { community: Community }) {
|
|
||||||
return <div>{props.community.community_title}</div>;
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -109,7 +109,7 @@
|
||||||
--chart-3: oklch(0.6 0.118 184.704);
|
--chart-3: oklch(0.6 0.118 184.704);
|
||||||
--chart-4: oklch(0.511 0.096 186.391);
|
--chart-4: oklch(0.511 0.096 186.391);
|
||||||
--chart-5: oklch(0.437 0.078 188.216);
|
--chart-5: oklch(0.437 0.078 188.216);
|
||||||
--sidebar: oklch(0.190 0 0);
|
--sidebar: oklch(0.19 0 0);
|
||||||
--sidebar-foreground: oklch(0.985 0 0);
|
--sidebar-foreground: oklch(0.985 0 0);
|
||||||
--sidebar-primary: oklch(0.704 0.14 182.503);
|
--sidebar-primary: oklch(0.704 0.14 182.503);
|
||||||
--sidebar-primary-foreground: oklch(0.277 0.046 192.524);
|
--sidebar-primary-foreground: oklch(0.277 0.046 192.524);
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,6 @@ import CallScreen from "@tensamin/call/screen";
|
||||||
import Login from "@/routes/screens/login";
|
import Login from "@/routes/screens/login";
|
||||||
import Signup from "@/routes/screens/signup";
|
import Signup from "@/routes/screens/signup";
|
||||||
|
|
||||||
import ConversationContext from "@/features/conversation/context";
|
|
||||||
import ChatContext from "@tensamin/chat/context";
|
import ChatContext from "@tensamin/chat/context";
|
||||||
import CallContext from "@tensamin/call/context";
|
import CallContext from "@tensamin/call/context";
|
||||||
import SocketContext from "@tensamin/ttp/context";
|
import SocketContext from "@tensamin/ttp/context";
|
||||||
|
|
@ -57,11 +56,9 @@ function AppShell() {
|
||||||
<SocketContext>
|
<SocketContext>
|
||||||
<UserContext>
|
<UserContext>
|
||||||
<CallContext>
|
<CallContext>
|
||||||
<ConversationContext>
|
<AppLayout>
|
||||||
<AppLayout>
|
<Outlet />
|
||||||
<Outlet />
|
</AppLayout>
|
||||||
</AppLayout>
|
|
||||||
</ConversationContext>
|
|
||||||
</CallContext>
|
</CallContext>
|
||||||
</UserContext>
|
</UserContext>
|
||||||
</SocketContext>
|
</SocketContext>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "tensamin",
|
"name": "tensamin",
|
||||||
|
"version": "0.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"packages/*",
|
"packages/*",
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { legalDocsSchema } from "./features/legal/schema";
|
import { legalDocsSchema } from "./features/legal/schema";
|
||||||
import { community, conversation } from "./features/conversation/schema";
|
|
||||||
|
|
||||||
const fileFromMessage = z.object({
|
const fileFromMessage = z.object({
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
|
|
@ -37,24 +36,38 @@ export const failedUser = {
|
||||||
// TTP
|
// TTP
|
||||||
export const ttp = {
|
export const ttp = {
|
||||||
identification: {
|
identification: {
|
||||||
request: z
|
request: z.object({
|
||||||
.object({
|
version: z.string(),
|
||||||
user_id: z.number().optional(),
|
session_id: z.number(),
|
||||||
iota_id: z.number().optional(),
|
user_id: z.number(),
|
||||||
})
|
}),
|
||||||
.refine(
|
|
||||||
(value) =>
|
|
||||||
(typeof value.user_id === "number") !==
|
|
||||||
(typeof value.iota_id === "number"),
|
|
||||||
{
|
|
||||||
message: "Either user_id or iota_id must be provided",
|
|
||||||
},
|
|
||||||
),
|
|
||||||
response: z.object({
|
response: z.object({
|
||||||
challenge: z.string(),
|
challenge: z.string(),
|
||||||
public_key: z.base64(),
|
public_key: z.base64(),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
challenge_response: {
|
||||||
|
request: z.object({
|
||||||
|
challenge: z.base64(),
|
||||||
|
}),
|
||||||
|
response: z.object({
|
||||||
|
communities: z.array(z.object({})).optional(),
|
||||||
|
contacts: z.array(
|
||||||
|
z.object({
|
||||||
|
calls: z.array(z.uuidv4()).optional(),
|
||||||
|
last_message_at: z.number(),
|
||||||
|
user_id: z.number(),
|
||||||
|
last_message: z
|
||||||
|
.object({
|
||||||
|
content: z.base64(),
|
||||||
|
sender_id: z.number(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
messages: z.array(message),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
},
|
||||||
get_user_data: {
|
get_user_data: {
|
||||||
request: z.object({
|
request: z.object({
|
||||||
user_id: z.number(),
|
user_id: z.number(),
|
||||||
|
|
@ -85,14 +98,6 @@ export const ttp = {
|
||||||
username: z.string().max(15),
|
username: z.string().max(15),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
challenge_response: {
|
|
||||||
request: z.object({
|
|
||||||
challenge: z.base64(),
|
|
||||||
}),
|
|
||||||
response: z.object({
|
|
||||||
accepted: z.boolean(),
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
ping: {
|
ping: {
|
||||||
request: z.object({
|
request: z.object({
|
||||||
last_ping: z.number(),
|
last_ping: z.number(),
|
||||||
|
|
@ -101,18 +106,6 @@ export const ttp = {
|
||||||
ping_iota: z.number(),
|
ping_iota: z.number(),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
get_conversations: {
|
|
||||||
request: z.object({}),
|
|
||||||
response: z.object({
|
|
||||||
user_ids: z.array(conversation),
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
get_communities: {
|
|
||||||
request: z.object({}),
|
|
||||||
response: z.object({
|
|
||||||
communities: z.array(community),
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
message_live: {
|
message_live: {
|
||||||
request: z.object({}),
|
request: z.object({}),
|
||||||
response: z.object({
|
response: z.object({
|
||||||
|
|
@ -166,6 +159,7 @@ export type TTP = typeof ttp;
|
||||||
|
|
||||||
// Storage
|
// Storage
|
||||||
export interface Storage {
|
export interface Storage {
|
||||||
|
session_id: number;
|
||||||
user_id: number;
|
user_id: number;
|
||||||
private_key: string;
|
private_key: string;
|
||||||
ppandtos_done: boolean;
|
ppandtos_done: boolean;
|
||||||
|
|
@ -179,6 +173,7 @@ export interface Storage {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const storageDefaults: Storage = {
|
export const storageDefaults: Storage = {
|
||||||
|
session_id: 0,
|
||||||
user_id: 0,
|
user_id: 0,
|
||||||
private_key: "",
|
private_key: "",
|
||||||
ppandtos_done: false,
|
ppandtos_done: false,
|
||||||
|
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
export const conversation = z.object({
|
|
||||||
user_id: z.number(),
|
|
||||||
calls: z.array(z.uuidv4()).optional(),
|
|
||||||
last_message_at: z.number(),
|
|
||||||
username: z.string().optional(), // Todo: Remove this
|
|
||||||
});
|
|
||||||
|
|
||||||
export const community = z.object({
|
|
||||||
community_address: z.string(),
|
|
||||||
community_title: z.string(),
|
|
||||||
position: z.string(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export type Conversation = z.infer<typeof conversation>;
|
|
||||||
export type Community = z.infer<typeof community>;
|
|
||||||
|
|
@ -15,189 +15,193 @@ type DataKind =
|
||||||
| { array: PrimitiveDataKind | "container" | "null" };
|
| { array: PrimitiveDataKind | "container" | "null" };
|
||||||
|
|
||||||
const COMMUNICATION_TYPES = [
|
const COMMUNICATION_TYPES = [
|
||||||
"error",
|
"error", // 0
|
||||||
"error_protocol",
|
"error_protocol", // 1
|
||||||
"error_anonymous",
|
"error_anonymous", // 2
|
||||||
"error_internal",
|
"error_internal", // 3
|
||||||
"error_invalid_data",
|
"error_invalid_data", // 4
|
||||||
"error_invalid_user_id",
|
"error_invalid_user_id", // 5
|
||||||
"error_invalid_omikron_id",
|
"error_invalid_omikron_id", // 6
|
||||||
"error_not_found",
|
"error_not_found", // 7
|
||||||
"error_not_authenticated",
|
"error_not_authenticated", // 8
|
||||||
"error_no_iota",
|
"error_no_iota", // 9
|
||||||
"error_invalid_challenge",
|
"error_invalid_challenge", // 10
|
||||||
"error_invalid_secret",
|
"error_invalid_secret", // 11
|
||||||
"error_invalid_private_key",
|
"error_invalid_private_key", // 12
|
||||||
"error_invalid_public_key",
|
"error_invalid_public_key", // 13
|
||||||
"error_no_user_id",
|
"error_no_user_id", // 14
|
||||||
"error_no_call_id",
|
"error_no_call_id", // 15
|
||||||
"error_invalid_call_id",
|
"error_invalid_call_id", // 16
|
||||||
"success",
|
"success", // 17
|
||||||
"shorten_link",
|
"shorten_link", // 18
|
||||||
"settings_save",
|
"settings_save", // 19
|
||||||
"settings_load",
|
"settings_load", // 20
|
||||||
"settings_list",
|
"settings_list", // 21
|
||||||
"message",
|
"message", // 22
|
||||||
"message_state",
|
"message_state", // 23
|
||||||
"message_send",
|
"message_send", // 24
|
||||||
"message_live",
|
"message_live", // 25
|
||||||
"message_other_iota",
|
"message_other_iota", // 26
|
||||||
"message_chunk",
|
"message_chunk", // 27
|
||||||
"messages_get",
|
"messages_get", // 28
|
||||||
"push_notification",
|
"push_notification", // 29
|
||||||
"read_notification",
|
"read_notification", // 30
|
||||||
"get_notifications",
|
"get_notifications", // 31
|
||||||
"change_confirm",
|
"change_confirm", // 32
|
||||||
"confirm_receive",
|
"confirm_receive", // 33
|
||||||
"confirm_read",
|
"confirm_read", // 34
|
||||||
"get_conversations",
|
"get_conversations", // 35
|
||||||
"get_states",
|
"get_states", // 36
|
||||||
"add_community",
|
"add_community", // 37
|
||||||
"remove_community",
|
"remove_community", // 38
|
||||||
"get_communities",
|
"get_communities", // 39
|
||||||
"challenge",
|
"challenge", // 40
|
||||||
"challenge_response",
|
"challenge_response", // 41
|
||||||
"register",
|
"register", // 42
|
||||||
"register_response",
|
"register_response", // 43
|
||||||
"identification",
|
"identification", // 44
|
||||||
"identification_response",
|
"identification_response", // 45
|
||||||
"register_iota",
|
"register_iota", // 46
|
||||||
"register_iota_success",
|
"register_iota_success", // 47
|
||||||
"ping",
|
"ping", // 48
|
||||||
"pong",
|
"pong", // 49
|
||||||
"add_conversation",
|
"add_conversation", // 50
|
||||||
"send_chat",
|
"send_chat", // 51
|
||||||
"client_changed",
|
"client_changed", // 52
|
||||||
"client_connected",
|
"client_connected", // 53
|
||||||
"client_disconnected",
|
"client_disconnected", // 54
|
||||||
"client_closed",
|
"client_closed", // 55
|
||||||
"public_key",
|
"public_key", // 56
|
||||||
"private_key",
|
"private_key", // 57
|
||||||
"webrtc_sdp",
|
"webrtc_sdp", // 58
|
||||||
"webrtc_ice",
|
"webrtc_ice", // 59
|
||||||
"start_stream",
|
"start_stream", // 60
|
||||||
"end_stream",
|
"end_stream", // 61
|
||||||
"watch_stream",
|
"watch_stream", // 62
|
||||||
"call_token",
|
"call_token", // 63
|
||||||
"call_invite",
|
"call_invite", // 64
|
||||||
"call_disconnect_user",
|
"call_disconnect_user", // 65
|
||||||
"call_timeout_user",
|
"call_timeout_user", // 66
|
||||||
"call_set_anonymous_joining",
|
"call_set_anonymous_joining", // 67
|
||||||
"end_call",
|
"end_call", // 68
|
||||||
"function",
|
"function", // 69
|
||||||
"update",
|
"update", // 70
|
||||||
"create_user",
|
"create_user", // 71
|
||||||
"rho_update",
|
"rho_update", // 72
|
||||||
"user_connected",
|
"user_connected", // 73
|
||||||
"user_disconnected",
|
"user_disconnected", // 74
|
||||||
"iota_connected",
|
"iota_connected", // 75
|
||||||
"iota_disconnected",
|
"iota_disconnected", // 76
|
||||||
"sync_client_iota_status",
|
"sync_client_iota_status", // 77
|
||||||
"get_user_data",
|
"get_user_data", // 78
|
||||||
"get_iota_data",
|
"get_iota_data", // 79
|
||||||
"iota_user_data",
|
"iota_user_data", // 80
|
||||||
"change_user_data",
|
"change_user_data", // 81
|
||||||
"change_iota_data",
|
"change_iota_data", // 82
|
||||||
"get_register",
|
"get_register", // 83
|
||||||
"complete_register_user",
|
"complete_register_user", // 84
|
||||||
"complete_register_iota",
|
"complete_register_iota", // 85
|
||||||
"delete_user",
|
"delete_user", // 86
|
||||||
"delete_iota",
|
"delete_iota", // 87
|
||||||
"start_register",
|
"start_register", // 88
|
||||||
"complete_register",
|
"complete_register", // 89
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
const DATA_TYPES = [
|
const DATA_TYPES = [
|
||||||
"error_type",
|
"error_type", // 0
|
||||||
"error_protocol",
|
"error_protocol", // 1
|
||||||
"accepted_ids",
|
"accepted_ids", // 2
|
||||||
"uuid",
|
"uuid", // 3
|
||||||
"register_id",
|
"register_id", // 4
|
||||||
"link",
|
"link", // 5
|
||||||
"settings",
|
"settings", // 6
|
||||||
"settings_name",
|
"settings_name", // 7
|
||||||
"chat_partner_id",
|
"chat_partner_id", // 8
|
||||||
"chat_partner_name",
|
"chat_partner_name", // 9
|
||||||
"iota_id",
|
"iota_id", // 10
|
||||||
"user_id",
|
"user_id", // 11
|
||||||
"user_ids",
|
"user_ids", // 12
|
||||||
"iota_ids",
|
"iota_ids", // 13
|
||||||
"user_state",
|
"user_state", // 14
|
||||||
"user_states",
|
"user_states", // 15
|
||||||
"user_pings",
|
"user_pings", // 16
|
||||||
"call_state",
|
"call_state", // 17
|
||||||
"screen_share",
|
"screen_share", // 18
|
||||||
"private_key_hash",
|
"private_key_hash", // 19
|
||||||
"accepted",
|
"accepted", // 20
|
||||||
"accepted_profiles",
|
"accepted_profiles", // 21
|
||||||
"denied_profiles",
|
"denied_profiles", // 22
|
||||||
"content",
|
"content", // 23
|
||||||
"messages",
|
"messages", // 24
|
||||||
"notifications",
|
"notifications", // 25
|
||||||
"timestamp",
|
"timestamp", // 26
|
||||||
"get_time",
|
"get_time", // 27
|
||||||
"get_variant",
|
"get_variant", // 28
|
||||||
"shared_secret_own",
|
"shared_secret_own", // 29
|
||||||
"shared_secret_other",
|
"shared_secret_other", // 30
|
||||||
"shared_secret_sign",
|
"shared_secret_sign", // 31
|
||||||
"shared_secret",
|
"shared_secret", // 32
|
||||||
"call_id",
|
"call_id", // 33
|
||||||
"call_token",
|
"call_token", // 34
|
||||||
"untill",
|
"untill", // 35
|
||||||
"enabled",
|
"enabled", // 36
|
||||||
"start_date",
|
"start_date", // 37
|
||||||
"end_date",
|
"end_date", // 38
|
||||||
"receiver_id",
|
"receiver_id", // 39
|
||||||
"sender_id",
|
"sender_id", // 40
|
||||||
"signature",
|
"signature", // 41
|
||||||
"signed",
|
"signed", // 42
|
||||||
"message",
|
"message", // 43
|
||||||
"message_state",
|
"message_state", // 44
|
||||||
"last_ping",
|
"last_ping", // 45
|
||||||
"ping_iota",
|
"ping_iota", // 46
|
||||||
"ping_clients",
|
"ping_clients", // 47
|
||||||
"matches",
|
"matches", // 48
|
||||||
"omikron",
|
"omikron", // 49
|
||||||
"offset",
|
"offset", // 50
|
||||||
"amount",
|
"amount", // 51
|
||||||
"position",
|
"position", // 52
|
||||||
"name",
|
"name", // 53
|
||||||
"path",
|
"path", // 54
|
||||||
"codec",
|
"codec", // 55
|
||||||
"function",
|
"function", // 56
|
||||||
"payload",
|
"payload", // 57
|
||||||
"result",
|
"result", // 58
|
||||||
"interactables",
|
"interactables", // 59
|
||||||
"want_to_watch",
|
"want_to_watch", // 60
|
||||||
"watcher",
|
"watcher", // 61
|
||||||
"created_at",
|
"created_at", // 62
|
||||||
"username",
|
"username", // 63
|
||||||
"display",
|
"display", // 64
|
||||||
"avatar",
|
"avatar", // 65
|
||||||
"about",
|
"about", // 66
|
||||||
"status",
|
"status", // 67
|
||||||
"public_key",
|
"public_key", // 68
|
||||||
"sub_level",
|
"sub_level", // 69
|
||||||
"sub_end",
|
"sub_end", // 70
|
||||||
"community_address",
|
"community_address", // 71
|
||||||
"challenge",
|
"challenge", // 72
|
||||||
"community_title",
|
"community_title", // 73
|
||||||
"communities",
|
"communities", // 74
|
||||||
"rho_connections",
|
"rho_connections", // 75
|
||||||
"user",
|
"user", // 76
|
||||||
"online_status",
|
"online_status", // 77
|
||||||
"omikron_id",
|
"omikron_id", // 78
|
||||||
"omikron_connections",
|
"omikron_connections", // 79
|
||||||
"reset_token",
|
"reset_token", // 80
|
||||||
"new_token",
|
"new_token", // 81
|
||||||
"call_invited",
|
"call_invited", // 82
|
||||||
"call_members",
|
"call_members", // 83
|
||||||
"calls",
|
"calls", // 84
|
||||||
"timeout",
|
"timeout", // 85
|
||||||
"has_admin",
|
"has_admin", // 86
|
||||||
"last_message_at",
|
"last_message_at", // 87
|
||||||
"height",
|
"height", // 88
|
||||||
"sent_by_self",
|
"sent_by_self", // 89
|
||||||
|
"session_id", // 90
|
||||||
|
"contacts", // 91
|
||||||
|
"last_message", // 92
|
||||||
|
"version", // 93
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
const COMMUNICATION_TYPE_BY_NAME = createIndexMap(COMMUNICATION_TYPES);
|
const COMMUNICATION_TYPE_BY_NAME = createIndexMap(COMMUNICATION_TYPES);
|
||||||
|
|
@ -227,6 +231,7 @@ registerDataKinds("number", [
|
||||||
"sub_end",
|
"sub_end",
|
||||||
"last_message_at",
|
"last_message_at",
|
||||||
"height",
|
"height",
|
||||||
|
"session_id",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
registerDataKinds("string", [
|
registerDataKinds("string", [
|
||||||
|
|
@ -260,6 +265,7 @@ registerDataKinds("string", [
|
||||||
"call_token",
|
"call_token",
|
||||||
"challenge",
|
"challenge",
|
||||||
"online_status",
|
"online_status",
|
||||||
|
"version",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
registerDataKinds({ array: "container" }, [
|
registerDataKinds({ array: "container" }, [
|
||||||
|
|
@ -269,6 +275,7 @@ registerDataKinds({ array: "container" }, [
|
||||||
"matches",
|
"matches",
|
||||||
"get_conversations",
|
"get_conversations",
|
||||||
"get_communities",
|
"get_communities",
|
||||||
|
"contacts",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
registerDataKinds({ array: "number" }, [
|
registerDataKinds({ array: "number" }, [
|
||||||
|
|
@ -289,6 +296,7 @@ registerDataKinds("container", [
|
||||||
"result",
|
"result",
|
||||||
"ping_clients",
|
"ping_clients",
|
||||||
"user_pings",
|
"user_pings",
|
||||||
|
"last_message",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
registerDataKinds("bool", [
|
registerDataKinds("bool", [
|
||||||
|
|
|
||||||
|
|
@ -20,10 +20,17 @@ import {
|
||||||
RETRY_INTERVAL,
|
RETRY_INTERVAL,
|
||||||
TRANSPORT_URL,
|
TRANSPORT_URL,
|
||||||
} from "./values";
|
} from "./values";
|
||||||
import { ttp as schemas, type TTP as Schemas } from "@tensamin/shared/data";
|
import {
|
||||||
|
ttp as schemas,
|
||||||
|
ttp,
|
||||||
|
type TTP as Schemas,
|
||||||
|
} from "@tensamin/shared/data";
|
||||||
import Loading from "@tensamin/ui/screens/loading";
|
import Loading from "@tensamin/ui/screens/loading";
|
||||||
import ErrorScreen from "@tensamin/ui/screens/error";
|
import ErrorScreen from "@tensamin/ui/screens/error";
|
||||||
|
|
||||||
|
import { version } from "../../../package.json";
|
||||||
|
import z from "zod";
|
||||||
|
|
||||||
const FATAL_IDENTIFICATION_ERROR_TYPES = new Set([
|
const FATAL_IDENTIFICATION_ERROR_TYPES = new Set([
|
||||||
"error",
|
"error",
|
||||||
"error_invalid_user_id",
|
"error_invalid_user_id",
|
||||||
|
|
@ -131,10 +138,14 @@ function getProtocolErrorDetails(error: unknown) {
|
||||||
type ContextType = {
|
type ContextType = {
|
||||||
send: BoundSendFn<Schemas>;
|
send: BoundSendFn<Schemas>;
|
||||||
subscribePush: (handler: PushHandler) => () => void;
|
subscribePush: (handler: PushHandler) => () => void;
|
||||||
readyState: () => number;
|
readyState: number;
|
||||||
ownPing: () => number;
|
ownPing: number;
|
||||||
iotaPing: () => number;
|
iotaPing: number;
|
||||||
identified: () => boolean;
|
identified: boolean;
|
||||||
|
contacts: z.infer<typeof ttp.challenge_response.response.shape.contacts>;
|
||||||
|
communities: z.infer<
|
||||||
|
typeof ttp.challenge_response.response.shape.communities
|
||||||
|
>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const TTPContext = createContext<ContextType | undefined>(undefined);
|
const TTPContext = createContext<ContextType | undefined>(undefined);
|
||||||
|
|
@ -162,6 +173,13 @@ export default function Provider(props: {
|
||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
const [errorDescription, setErrorDescription] = useState("");
|
const [errorDescription, setErrorDescription] = useState("");
|
||||||
|
|
||||||
|
const [communities, setCommunities] = useState<
|
||||||
|
z.infer<typeof ttp.challenge_response.response.shape.communities>
|
||||||
|
>([]);
|
||||||
|
const [contacts, setContacts] = useState<
|
||||||
|
z.infer<typeof ttp.challenge_response.response.shape.contacts>
|
||||||
|
>([]);
|
||||||
|
|
||||||
const clientRef = useRef<ReturnType<
|
const clientRef = useRef<ReturnType<
|
||||||
typeof createTransportClient<Schemas>
|
typeof createTransportClient<Schemas>
|
||||||
> | null>(null);
|
> | null>(null);
|
||||||
|
|
@ -352,9 +370,7 @@ export default function Provider(props: {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
log(0, "ttp", "red", "Disconnected", closeError, {
|
log(0, "ttp", "red", "Disconnected", closeError);
|
||||||
stopSending: isStopSendingError(closeError),
|
|
||||||
});
|
|
||||||
scheduleReconnect(closeError);
|
scheduleReconnect(closeError);
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
@ -426,40 +442,69 @@ export default function Provider(props: {
|
||||||
*/
|
*/
|
||||||
const identify = async () => {
|
const identify = async () => {
|
||||||
try {
|
try {
|
||||||
|
const sessionId = await load("session_id");
|
||||||
const userId = await load("user_id");
|
const userId = await load("user_id");
|
||||||
const privateKey = await load("private_key");
|
const privateKey = await load("private_key");
|
||||||
|
|
||||||
if (!Number.isSafeInteger(userId) || userId <= 0) {
|
if (
|
||||||
throw new Error("Missing or invalid user id for identification");
|
!Number.isSafeInteger(userId) ||
|
||||||
}
|
userId <= 0 ||
|
||||||
|
privateKey.trim() === "" ||
|
||||||
if (privateKey.trim() === "") {
|
!Number.isSafeInteger(sessionId) ||
|
||||||
throw new Error("Missing private key for identification");
|
sessionId <= 0
|
||||||
|
) {
|
||||||
|
throw new Error("Invalid credentials");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Challenge Request
|
||||||
const challengeEnvelope = await send("identification", {
|
const challengeEnvelope = await send("identification", {
|
||||||
|
version,
|
||||||
|
session_id: sessionId,
|
||||||
user_id: userId,
|
user_id: userId,
|
||||||
|
}).catch((challengeError) => {
|
||||||
|
throw new Error(
|
||||||
|
"Failed to obtain identification challenge from server",
|
||||||
|
challengeError,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Shared Secret
|
||||||
const sharedSecret = await getSharedSecret(
|
const sharedSecret = await getSharedSecret(
|
||||||
privateKey,
|
privateKey,
|
||||||
"",
|
"",
|
||||||
challengeEnvelope.data.public_key,
|
challengeEnvelope.data.public_key,
|
||||||
);
|
).catch((secretError) => {
|
||||||
|
throw new Error(
|
||||||
|
"Failed to derive shared secret for identification",
|
||||||
|
secretError,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Challenge Decryption
|
||||||
const decryptedChallenge = await decrypt(
|
const decryptedChallenge = await decrypt(
|
||||||
sharedSecret,
|
sharedSecret,
|
||||||
challengeEnvelope.data.challenge,
|
challengeEnvelope.data.challenge,
|
||||||
);
|
).catch((decryptionError) => {
|
||||||
|
throw new Error(
|
||||||
const verification = await send("challenge_response", {
|
"Failed to decrypt identification challenge",
|
||||||
challenge: decryptedChallenge,
|
decryptionError,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (verification.data.accepted !== true) {
|
// Challenge Response
|
||||||
throw new Error("Identification challenge was rejected");
|
const finalResponse = await send("challenge_response", {
|
||||||
}
|
challenge: decryptedChallenge,
|
||||||
|
}).catch((error) => {
|
||||||
|
setError("Identification Failed");
|
||||||
|
setErrorDescription(
|
||||||
|
"Unable to complete secure identification. Please verify your credentials and try again.",
|
||||||
|
);
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Data handling
|
||||||
|
setContacts(finalResponse.data.contacts);
|
||||||
|
setCommunities(finalResponse.data.communities);
|
||||||
if (cancelled) {
|
if (cancelled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -547,18 +592,6 @@ export default function Provider(props: {
|
||||||
return undefined;
|
return undefined;
|
||||||
}, [connected, identified, identifying, readyState]);
|
}, [connected, identified, identifying, readyState]);
|
||||||
|
|
||||||
const contextValue = useMemo<ContextType>(
|
|
||||||
() => ({
|
|
||||||
send,
|
|
||||||
subscribePush,
|
|
||||||
readyState: () => readyState,
|
|
||||||
ownPing: () => ownPing,
|
|
||||||
iotaPing: () => iotaPing,
|
|
||||||
identified: () => identified,
|
|
||||||
}),
|
|
||||||
[identified, iotaPing, ownPing, readyState, send, subscribePush],
|
|
||||||
);
|
|
||||||
|
|
||||||
if (error !== "" && errorDescription !== "") {
|
if (error !== "" && errorDescription !== "") {
|
||||||
return <ErrorScreen error={error} description={errorDescription} />;
|
return <ErrorScreen error={error} description={errorDescription} />;
|
||||||
}
|
}
|
||||||
|
|
@ -575,7 +608,18 @@ export default function Provider(props: {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TTPContext.Provider value={contextValue}>
|
<TTPContext.Provider
|
||||||
|
value={{
|
||||||
|
send,
|
||||||
|
subscribePush,
|
||||||
|
readyState,
|
||||||
|
ownPing,
|
||||||
|
iotaPing,
|
||||||
|
identified,
|
||||||
|
contacts,
|
||||||
|
communities,
|
||||||
|
}}
|
||||||
|
>
|
||||||
{props.children}
|
{props.children}
|
||||||
</TTPContext.Provider>
|
</TTPContext.Provider>
|
||||||
);
|
);
|
||||||
|
|
|
||||||