Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1a71e2ddbf | |||
| 21274991f3 | |||
| 955b4d53da | |||
| 9f483ce8d3 | |||
| 8b1b34db6a |
|
|
@ -168,7 +168,7 @@ jobs:
|
||||||
test -d releases
|
test -d releases
|
||||||
find releases -type f | grep -q .
|
find releases -type f | grep -q .
|
||||||
|
|
||||||
LATEST_PROD_TAG="$(git for-each-ref refs/tags --sort=-creatordate --format='%(refname:short)' | awk '!/-/' | head -n 1 || true)"
|
LATEST_PROD_TAG="$(git tag --list '*-prod' --sort=-v:refname | head -n 1 || true)"
|
||||||
|
|
||||||
if [ -n "$LATEST_PROD_TAG" ]; then
|
if [ -n "$LATEST_PROD_TAG" ]; then
|
||||||
RAW_LOG="$(git log "$LATEST_PROD_TAG"..HEAD --pretty=format:'- %s')"
|
RAW_LOG="$(git log "$LATEST_PROD_TAG"..HEAD --pretty=format:'- %s')"
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 53 KiB |
|
Before Width: | Height: | Size: 7.7 MiB |
|
|
@ -1,8 +0,0 @@
|
||||||
{
|
|
||||||
"default": "./logo.svg",
|
|
||||||
|
|
||||||
"android_fg": "./android.png",
|
|
||||||
"android_bg": "./background.png",
|
|
||||||
"android_fg_scale": 100,
|
|
||||||
"android_monochrome": "./monochrome.png"
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 11 KiB |
|
|
@ -30,7 +30,7 @@
|
||||||
"build:desktop:raw": "tauri build",
|
"build:desktop:raw": "tauri build",
|
||||||
"dev:desktop": "if command -v nix >/dev/null 2>&1; then nix develop --command bun dev:desktop:raw; else bun dev:desktop:raw; fi",
|
"dev:desktop": "if command -v nix >/dev/null 2>&1; then nix develop --command bun dev:desktop:raw; else bun dev:desktop:raw; fi",
|
||||||
"build:desktop": "if command -v nix >/dev/null 2>&1; then nix develop --command bun build:desktop:raw; else bun build:desktop:raw; fi",
|
"build:desktop": "if command -v nix >/dev/null 2>&1; then nix develop --command bun build:desktop:raw; else bun build:desktop:raw; fi",
|
||||||
"gen-icons": "tauri icon ./logo.json",
|
"gen-icons": "tauri icon ./logo.svg",
|
||||||
"format": "bunx prettier --write .",
|
"format": "bunx prettier --write .",
|
||||||
"lint": "eslint src"
|
"lint": "eslint src"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,9 @@ const tauriConfigPath = path.resolve(__dirname, "./src-tauri/tauri.conf.json");
|
||||||
const isUnrender = process.argv.includes("--unrender");
|
const isUnrender = process.argv.includes("--unrender");
|
||||||
|
|
||||||
// Version Source
|
// Version Source
|
||||||
const packageJson = JSON.parse(fs.readFileSync(rootPackageJsonPath, "utf8"));
|
const packageJson = JSON.parse(
|
||||||
|
fs.readFileSync(rootPackageJsonPath, "utf8")
|
||||||
|
);
|
||||||
|
|
||||||
const packageVersion: string = packageJson.version;
|
const packageVersion: string = packageJson.version;
|
||||||
|
|
||||||
|
|
@ -20,7 +22,9 @@ if (!packageVersion && !isUnrender) {
|
||||||
throw new Error("No version found in package.json");
|
throw new Error("No version found in package.json");
|
||||||
}
|
}
|
||||||
|
|
||||||
const targetVersion = isUnrender ? PLACEHOLDER_VERSION : packageVersion;
|
const targetVersion = isUnrender
|
||||||
|
? PLACEHOLDER_VERSION
|
||||||
|
: packageVersion;
|
||||||
|
|
||||||
// Helpers
|
// Helpers
|
||||||
function updateCargoToml(content: string): string {
|
function updateCargoToml(content: string): string {
|
||||||
|
|
@ -30,7 +34,10 @@ function updateCargoToml(content: string): string {
|
||||||
throw new Error("Could not find version field in Cargo.toml");
|
throw new Error("Could not find version field in Cargo.toml");
|
||||||
}
|
}
|
||||||
|
|
||||||
return content.replace(regex, `version = "${targetVersion}"`);
|
return content.replace(
|
||||||
|
regex,
|
||||||
|
`version = "${targetVersion}"`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateTauriConfig(content: string): string {
|
function updateTauriConfig(content: string): string {
|
||||||
|
|
@ -40,7 +47,10 @@ function updateTauriConfig(content: string): string {
|
||||||
throw new Error("Could not find version field in tauri.conf.json");
|
throw new Error("Could not find version field in tauri.conf.json");
|
||||||
}
|
}
|
||||||
|
|
||||||
return content.replace(regex, `"version": "${targetVersion}"`);
|
return content.replace(
|
||||||
|
regex,
|
||||||
|
`"version": "${targetVersion}"`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update Cargo.toml
|
// Update Cargo.toml
|
||||||
|
|
|
||||||
2
apps/tauri/src-tauri/Cargo.lock
generated
|
|
@ -4838,7 +4838,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tensamin"
|
name = "tensamin"
|
||||||
version = "0.0.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"image 0.25.10",
|
"image 0.25.10",
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
[package]
|
[package]
|
||||||
name = "tensamin"
|
name = "Tensamin"
|
||||||
version = "0.0.0"
|
version = "0.0.0"
|
||||||
description = "Privacy focused messanger"
|
description = "Privacy focused messanger"
|
||||||
authors = ["methanium"]
|
authors = ["methanium"]
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||||
<background android:drawable="@mipmap/ic_launcher_background"/>
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
<monochrome android:drawable="@mipmap/ic_launcher_monochrome"/>
|
|
||||||
</adaptive-icon>
|
</adaptive-icon>
|
||||||
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 758 B |
|
Before Width: | Height: | Size: 6 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 9.3 KiB |
|
Before Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 9.7 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 9.6 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 25 KiB |
|
|
@ -1,4 +1,4 @@
|
||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">Tensamin</string>
|
<string name="app_name">tensamin</string>
|
||||||
<string name="main_activity_title">Tensamin</string>
|
<string name="main_activity_title">tensamin</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
@ -7,7 +7,6 @@ import {
|
||||||
} from "react";
|
} from "react";
|
||||||
import { getCurrent, onOpenUrl } from "@tauri-apps/plugin-deep-link";
|
import { getCurrent, onOpenUrl } from "@tauri-apps/plugin-deep-link";
|
||||||
import { isTauri } from "@tauri-apps/api/core";
|
import { isTauri } from "@tauri-apps/api/core";
|
||||||
import { useIsMobile } from "@tensamin/ui";
|
|
||||||
|
|
||||||
type DeeplinkContextValue = {
|
type DeeplinkContextValue = {
|
||||||
deeplinks: readonly string[];
|
deeplinks: readonly string[];
|
||||||
|
|
@ -32,10 +31,10 @@ export default function DeeplinkProvider({
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
}) {
|
}) {
|
||||||
const [deeplinks, setDeeplinks] = useState<string[]>([]);
|
const [deeplinks, setDeeplinks] = useState<string[]>([]);
|
||||||
const isMobile = useIsMobile();
|
const isTauriEnv = isTauri();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isTauri() || !isMobile) return;
|
if (!isTauriEnv) return;
|
||||||
|
|
||||||
let mounted = true;
|
let mounted = true;
|
||||||
let unlisten: (() => void) | undefined;
|
let unlisten: (() => void) | undefined;
|
||||||
|
|
@ -58,7 +57,7 @@ export default function DeeplinkProvider({
|
||||||
mounted = false;
|
mounted = false;
|
||||||
unlisten?.();
|
unlisten?.();
|
||||||
};
|
};
|
||||||
}, [isMobile]);
|
}, [isTauriEnv]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<deeplinkContext.Provider
|
<deeplinkContext.Provider
|
||||||
|
|
|
||||||
|
|
@ -1,34 +0,0 @@
|
||||||
import type { User } from "@tensamin/user/context";
|
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from "@tensamin/ui";
|
|
||||||
import Text from "@tensamin/markdown/text";
|
|
||||||
|
|
||||||
export default function Profile({ user }: { user: User }) {
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<div className="flex gap-2 items-center">
|
|
||||||
<Avatar className="size-10">
|
|
||||||
<AvatarImage src={user.avatar} />
|
|
||||||
<AvatarFallback className="text-lg">
|
|
||||||
{user.display.slice(0, 2).toUpperCase()}
|
|
||||||
</AvatarFallback>
|
|
||||||
</Avatar>
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<p className="text-lg font-semibold">{user.display}</p>
|
|
||||||
<p className="text-muted-foreground">{user.username}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Text value={user.about || ""} />
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<p className="text-muted-foreground text-xs overflow-hidden text-ellipsis whitespace-nowrap">
|
|
||||||
iota: {user.iota_id}
|
|
||||||
</p>
|
|
||||||
<p className="text-muted-foreground text-xs overflow-hidden text-ellipsis whitespace-nowrap">
|
|
||||||
user: {user.user_id}
|
|
||||||
</p>
|
|
||||||
<p className="text-muted-foreground text-xs overflow-hidden text-ellipsis whitespace-nowrap">
|
|
||||||
pub key: {user.public_key}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
import { Button, Popover, PopoverContent, PopoverTrigger } from "@tensamin/ui";
|
import { Button } from "@tensamin/ui";
|
||||||
import {
|
import {
|
||||||
ArrowLeft,
|
ArrowLeft,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
ChevronUp,
|
|
||||||
House,
|
House,
|
||||||
Phone,
|
Phone,
|
||||||
Settings,
|
Settings,
|
||||||
|
|
@ -18,7 +17,6 @@ import { useState } from "react";
|
||||||
import { SidebarTrigger, useSidebar } from "@tensamin/ui";
|
import { SidebarTrigger, useSidebar } from "@tensamin/ui";
|
||||||
import { WindowControls as Controls } from "@tensamin/ui";
|
import { WindowControls as Controls } from "@tensamin/ui";
|
||||||
import { useSession } from "@tensamin/storage/session";
|
import { useSession } from "@tensamin/storage/session";
|
||||||
import Profile from "./modals/profile";
|
|
||||||
|
|
||||||
export default function Navbar({ forMobile }: { forMobile: boolean }) {
|
export default function Navbar({ forMobile }: { forMobile: boolean }) {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
@ -35,8 +33,6 @@ export default function Navbar({ forMobile }: { forMobile: boolean }) {
|
||||||
|
|
||||||
const [selectOpen, setSelectOpen] = useState(false);
|
const [selectOpen, setSelectOpen] = useState(false);
|
||||||
|
|
||||||
const [userInfoOpen, setUserInfoOpen] = useState(false);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-tauri-drag-region
|
data-tauri-drag-region
|
||||||
|
|
@ -73,10 +69,6 @@ export default function Navbar({ forMobile }: { forMobile: boolean }) {
|
||||||
<Wrapper
|
<Wrapper
|
||||||
userId={id}
|
userId={id}
|
||||||
component={(user) => (
|
component={(user) => (
|
||||||
<>
|
|
||||||
<Popover open={userInfoOpen} onOpenChange={setUserInfoOpen}>
|
|
||||||
<PopoverTrigger
|
|
||||||
render={
|
|
||||||
<Button
|
<Button
|
||||||
variant="link"
|
variant="link"
|
||||||
className="px-0! text-foreground"
|
className="px-0! text-foreground"
|
||||||
|
|
@ -85,15 +77,8 @@ export default function Navbar({ forMobile }: { forMobile: boolean }) {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<p className="font-medium text-md">{user?.display}</p>
|
<p className="font-medium text-md">{user?.display}</p>
|
||||||
{userInfoOpen ? <ChevronUp /> : <ChevronDown />}
|
<ChevronDown />
|
||||||
</Button>
|
</Button>
|
||||||
}
|
|
||||||
/>
|
|
||||||
<PopoverContent side="bottom">
|
|
||||||
<Profile user={user} />
|
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
loading={<Skeleton className="ml-3 w-40 h-5" />}
|
loading={<Skeleton className="ml-3 w-40 h-5" />}
|
||||||
/>
|
/>
|
||||||
|
|
@ -156,7 +141,7 @@ export default function Navbar({ forMobile }: { forMobile: boolean }) {
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<Controls className="mr-2" />
|
<Controls className="mr-2 ring-border" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Button, cn, useIsMobile } from "@tensamin/ui";
|
import { Button, cn } from "@tensamin/ui";
|
||||||
import { Input } from "@tensamin/ui";
|
import { Input } from "@tensamin/ui";
|
||||||
import { Label } from "@tensamin/ui";
|
import { Label } from "@tensamin/ui";
|
||||||
import { useStorage } from "@tensamin/storage/context";
|
import { useStorage } from "@tensamin/storage/context";
|
||||||
|
|
@ -84,7 +84,6 @@ function parseTuFileContent(rawFileContent: string): {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Form() {
|
export default function Form() {
|
||||||
const isMobile = useIsMobile();
|
|
||||||
const uploadRef = React.useRef<HTMLInputElement | null>(null);
|
const uploadRef = React.useRef<HTMLInputElement | null>(null);
|
||||||
const [isDragging, setIsDragging] = React.useState(false);
|
const [isDragging, setIsDragging] = React.useState(false);
|
||||||
const { save } = useStorage();
|
const { save } = useStorage();
|
||||||
|
|
@ -267,7 +266,7 @@ export default function Form() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative flex md:flex-row flex-col gap-15">
|
<div className="relative flex md:flex-row flex-col gap-15">
|
||||||
{isTauri() && isMobile ? (
|
{isTauri() ? (
|
||||||
<>
|
<>
|
||||||
<QrCodeScanner
|
<QrCodeScanner
|
||||||
onData={async (data) => {
|
onData={async (data) => {
|
||||||
|
|
|
||||||
1
bun.lock
|
|
@ -171,7 +171,6 @@
|
||||||
"name": "@tensamin/notifications",
|
"name": "@tensamin/notifications",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tanstack/react-router": "^1.169.1",
|
|
||||||
"@tauri-apps/api": "^2.11.0",
|
"@tauri-apps/api": "^2.11.0",
|
||||||
"@tensamin/chat": "workspace:*",
|
"@tensamin/chat": "workspace:*",
|
||||||
"@tensamin/crypto": "workspace:*",
|
"@tensamin/crypto": "workspace:*",
|
||||||
|
|
|
||||||
|
|
@ -295,9 +295,7 @@ async function updateLocalParticipantAttributes(
|
||||||
screenSharePreviewLength: attributes.screenSharePreview?.length ?? 0,
|
screenSharePreviewLength: attributes.screenSharePreview?.length ?? 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
await getRoom()
|
await getRoom().localParticipant.setAttributes(attributes).catch((error) => {
|
||||||
.localParticipant.setAttributes(attributes)
|
|
||||||
.catch((error) => {
|
|
||||||
log(1, "call", "red", "Failed to update local participant attributes", {
|
log(1, "call", "red", "Failed to update local participant attributes", {
|
||||||
attributes: Object.keys(attributes),
|
attributes: Object.keys(attributes),
|
||||||
error,
|
error,
|
||||||
|
|
@ -797,9 +795,7 @@ export async function connect(callId: string) {
|
||||||
|
|
||||||
syncAllRemoteTrackSubscriptions();
|
syncAllRemoteTrackSubscriptions();
|
||||||
|
|
||||||
await getRoom()
|
await getRoom().localParticipant.setMicrophoneEnabled(true).catch((error) => {
|
||||||
.localParticipant.setMicrophoneEnabled(true)
|
|
||||||
.catch((error) => {
|
|
||||||
log(1, "call", "red", "Failed to enable microphone", error);
|
log(1, "call", "red", "Failed to enable microphone", error);
|
||||||
toast("error", "Failed to enable microphone.");
|
toast("error", "Failed to enable microphone.");
|
||||||
throw error;
|
throw error;
|
||||||
|
|
|
||||||
|
|
@ -11,3 +11,4 @@
|
||||||
- Desktop-App screenshares
|
- Desktop-App screenshares
|
||||||
- Context menus
|
- Context menus
|
||||||
- Popout Window
|
- Popout Window
|
||||||
|
- If micGated=true & isSpeaking=false for 5 seconds show banner with mic detection
|
||||||
|
|
|
||||||
|
|
@ -97,7 +97,6 @@ export default function InputComponent({
|
||||||
>
|
>
|
||||||
<CardHeader className="relative p-0 flex flex-col">
|
<CardHeader className="relative p-0 flex flex-col">
|
||||||
<Input
|
<Input
|
||||||
className="w-full"
|
|
||||||
paddingY="13px"
|
paddingY="13px"
|
||||||
paddingX="13px"
|
paddingX="13px"
|
||||||
placeholder="Send a message..."
|
placeholder="Send a message..."
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ import Message from "./components/message";
|
||||||
|
|
||||||
import { PAGE_SIZE } from "./values";
|
import { PAGE_SIZE } from "./values";
|
||||||
import { useIsMobile } from "@tensamin/ui";
|
import { useIsMobile } from "@tensamin/ui";
|
||||||
import { Loader2 } from "lucide-react";
|
|
||||||
|
|
||||||
function getDistanceFromBottom(element: HTMLDivElement) {
|
function getDistanceFromBottom(element: HTMLDivElement) {
|
||||||
return element.scrollHeight - (element.scrollTop + element.clientHeight);
|
return element.scrollHeight - (element.scrollTop + element.clientHeight);
|
||||||
|
|
@ -102,6 +101,12 @@ export default function Screen() {
|
||||||
const virtualRowCount =
|
const virtualRowCount =
|
||||||
messages.length + (shouldShowConversationStart ? 1 : 0);
|
messages.length + (shouldShowConversationStart ? 1 : 0);
|
||||||
|
|
||||||
|
const messagesRef = React.useRef(messages);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
messagesRef.current = messages;
|
||||||
|
}, [messages]);
|
||||||
|
|
||||||
const getItemKey = React.useCallback(
|
const getItemKey = React.useCallback(
|
||||||
(index: number) => {
|
(index: number) => {
|
||||||
if (shouldShowConversationStart && index === 0) {
|
if (shouldShowConversationStart && index === 0) {
|
||||||
|
|
@ -109,9 +114,9 @@ export default function Screen() {
|
||||||
}
|
}
|
||||||
|
|
||||||
const messageIndex = shouldShowConversationStart ? index - 1 : index;
|
const messageIndex = shouldShowConversationStart ? index - 1 : index;
|
||||||
return messages[messageIndex]?.send_time ?? index;
|
return messagesRef.current[messageIndex]?.send_time ?? index;
|
||||||
},
|
},
|
||||||
[shouldShowConversationStart, messages],
|
[shouldShowConversationStart],
|
||||||
);
|
);
|
||||||
|
|
||||||
const setMeasurementRef = React.useCallback(
|
const setMeasurementRef = React.useCallback(
|
||||||
|
|
@ -355,38 +360,9 @@ export default function Screen() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let rafId: number;
|
|
||||||
let stableFrames = 0;
|
|
||||||
let lastScrollHeight = 0;
|
|
||||||
|
|
||||||
const settleToBottom = () => {
|
|
||||||
if (!scrollRef.current || hasScrolledToBottomInitially) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
||||||
const currentScrollHeight = scrollRef.current.scrollHeight;
|
|
||||||
|
|
||||||
if (currentScrollHeight === lastScrollHeight) {
|
|
||||||
stableFrames++;
|
|
||||||
} else {
|
|
||||||
stableFrames = 0;
|
|
||||||
lastScrollHeight = currentScrollHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (stableFrames >= 2) {
|
|
||||||
isAtBottomRef.current = true;
|
isAtBottomRef.current = true;
|
||||||
setHasScrolledToBottomInitially(true);
|
setHasScrolledToBottomInitially(true);
|
||||||
} else {
|
|
||||||
rafId = requestAnimationFrame(settleToBottom);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
settleToBottom();
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
cancelAnimationFrame(rafId);
|
|
||||||
};
|
|
||||||
}, [hasScrolledToBottomInitially, virtualRowCount]);
|
}, [hasScrolledToBottomInitially, virtualRowCount]);
|
||||||
|
|
||||||
React.useLayoutEffect(() => {
|
React.useLayoutEffect(() => {
|
||||||
|
|
@ -466,19 +442,6 @@ export default function Screen() {
|
||||||
}}
|
}}
|
||||||
onScroll={handleContainerScroll}
|
onScroll={handleContainerScroll}
|
||||||
>
|
>
|
||||||
{messagesQuery.isPending && (
|
|
||||||
<div className="flex items-center justify-center pt-10">
|
|
||||||
<p className="text-foreground/45 flex gap-1 items-center justify-center">
|
|
||||||
<Loader2 size={18} className="animate-spin" />
|
|
||||||
Loading...
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{messagesQuery.isFetchingNextPage && (
|
|
||||||
<div className="flex items-center justify-center pt-3 pb-1">
|
|
||||||
<Loader2 size={16} className="animate-spin text-foreground/45" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div
|
<div
|
||||||
className="relative w-full"
|
className="relative w-full"
|
||||||
style={{
|
style={{
|
||||||
|
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
- Add context menu to messages
|
|
||||||
|
|
@ -12,7 +12,6 @@
|
||||||
"build": "tsc -p tsconfig.json --noEmit"
|
"build": "tsc -p tsconfig.json --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tanstack/react-router": "^1.169.1",
|
|
||||||
"@tauri-apps/api": "^2.11.0",
|
"@tauri-apps/api": "^2.11.0",
|
||||||
"@tensamin/crypto": "workspace:*",
|
"@tensamin/crypto": "workspace:*",
|
||||||
"@tensamin/shared": "workspace:*",
|
"@tensamin/shared": "workspace:*",
|
||||||
|
|
|
||||||
|
|
@ -10,17 +10,16 @@ import { message as messageSchema } from "@tensamin/shared/data";
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from "@tensamin/ui";
|
import { Avatar, AvatarFallback, AvatarImage } from "@tensamin/ui";
|
||||||
import { isTauri } from "@tauri-apps/api/core";
|
import { isTauri } from "@tauri-apps/api/core";
|
||||||
import { useSession } from "@tensamin/storage/session";
|
import { useSession } from "@tensamin/storage/session";
|
||||||
import { useNavigate } from "@tanstack/react-router";
|
|
||||||
|
|
||||||
export const context = createContext<contextType | undefined>(undefined);
|
export const context = createContext<contextType | undefined>(undefined);
|
||||||
|
|
||||||
async function requestNotificationPermission() {
|
function reduceDisplay(display: string) {
|
||||||
if (!("Notification" in window)) return false;
|
const words = display.split(" ");
|
||||||
|
if (words.length === 1) {
|
||||||
if (Notification.permission === "granted") return true;
|
return display.slice(0, 2).toUpperCase();
|
||||||
|
} else {
|
||||||
const permission = await Notification.requestPermission();
|
return words[0].charAt(0).toUpperCase() + words[1].charAt(0).toUpperCase();
|
||||||
return permission === "granted";
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Provider(props: { children: React.ReactNode }) {
|
export default function Provider(props: { children: React.ReactNode }) {
|
||||||
|
|
@ -30,7 +29,6 @@ export default function Provider(props: { children: React.ReactNode }) {
|
||||||
const { decryptText, getSharedSecret } = useCrypto();
|
const { decryptText, getSharedSecret } = useCrypto();
|
||||||
const { addLiveMessage, userId } = useChat();
|
const { addLiveMessage, userId } = useChat();
|
||||||
const { moveUserIdToTop } = useSession();
|
const { moveUserIdToTop } = useSession();
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return subscribePush(async (ttpMessage) => {
|
return subscribePush(async (ttpMessage) => {
|
||||||
|
|
@ -56,7 +54,7 @@ export default function Provider(props: { children: React.ReactNode }) {
|
||||||
(await load("settings.read_confirmations")) &&
|
(await load("settings.read_confirmations")) &&
|
||||||
userId === sender_id
|
userId === sender_id
|
||||||
) {
|
) {
|
||||||
void send(
|
await send(
|
||||||
"message_state",
|
"message_state",
|
||||||
{
|
{
|
||||||
message_state: "read",
|
message_state: "read",
|
||||||
|
|
@ -66,7 +64,7 @@ export default function Provider(props: { children: React.ReactNode }) {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
void send(
|
await send(
|
||||||
"message_state",
|
"message_state",
|
||||||
{
|
{
|
||||||
message_state: "received",
|
message_state: "received",
|
||||||
|
|
@ -93,26 +91,6 @@ export default function Provider(props: { children: React.ReactNode }) {
|
||||||
|
|
||||||
if (isTauri()) {
|
if (isTauri()) {
|
||||||
console.log("weewoo");
|
console.log("weewoo");
|
||||||
} else {
|
|
||||||
const hasPermissions = await requestNotificationPermission();
|
|
||||||
|
|
||||||
if (hasPermissions) {
|
|
||||||
const notification = new Notification(user.display, {
|
|
||||||
body: decryptedContent,
|
|
||||||
icon: user.avatar || user.display.slice(0, 2).toUpperCase(),
|
|
||||||
badge: user.avatar || user.display.slice(0, 2).toUpperCase(),
|
|
||||||
tag: `message-${user.user_id}`,
|
|
||||||
silent: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
notification.onclick = () => {
|
|
||||||
window.focus();
|
|
||||||
navigate({
|
|
||||||
to: `/chat?id=${user.user_id}`,
|
|
||||||
});
|
|
||||||
|
|
||||||
notification.close();
|
|
||||||
};
|
|
||||||
} else {
|
} else {
|
||||||
sonnerToast(user.display, {
|
sonnerToast(user.display, {
|
||||||
classNames: {
|
classNames: {
|
||||||
|
|
@ -122,15 +100,12 @@ export default function Provider(props: { children: React.ReactNode }) {
|
||||||
icon: (
|
icon: (
|
||||||
<Avatar>
|
<Avatar>
|
||||||
<AvatarImage src={user.avatar} />
|
<AvatarImage src={user.avatar} />
|
||||||
<AvatarFallback>
|
<AvatarFallback>{reduceDisplay(user.display)}</AvatarFallback>
|
||||||
{user.display.slice(0, 2).toUpperCase()}
|
|
||||||
</AvatarFallback>
|
|
||||||
</Avatar>
|
</Avatar>
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}, [
|
}, [
|
||||||
subscribePush,
|
subscribePush,
|
||||||
|
|
@ -142,7 +117,6 @@ export default function Provider(props: { children: React.ReactNode }) {
|
||||||
userId,
|
userId,
|
||||||
moveUserIdToTop,
|
moveUserIdToTop,
|
||||||
send,
|
send,
|
||||||
navigate,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -257,7 +257,7 @@ export function Provider(props: {
|
||||||
setErrorDescription(
|
setErrorDescription(
|
||||||
"You could try to restart your Iota, check for updates or check your network connection.",
|
"You could try to restart your Iota, check for updates or check your network connection.",
|
||||||
);
|
);
|
||||||
});
|
})
|
||||||
}, [connected, subscribePush]);
|
}, [connected, subscribePush]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
||||||