(feat): add native notifications
(feat): add navbar profile popover (fix): chat input box click area to slim (qol): format
This commit is contained in:
parent
67b7926dab
commit
7ea3b32286
9 changed files with 131 additions and 59 deletions
|
|
@ -12,9 +12,7 @@ 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(
|
const packageJson = JSON.parse(fs.readFileSync(rootPackageJsonPath, "utf8"));
|
||||||
fs.readFileSync(rootPackageJsonPath, "utf8")
|
|
||||||
);
|
|
||||||
|
|
||||||
const packageVersion: string = packageJson.version;
|
const packageVersion: string = packageJson.version;
|
||||||
|
|
||||||
|
|
@ -22,9 +20,7 @@ if (!packageVersion && !isUnrender) {
|
||||||
throw new Error("No version found in package.json");
|
throw new Error("No version found in package.json");
|
||||||
}
|
}
|
||||||
|
|
||||||
const targetVersion = isUnrender
|
const targetVersion = isUnrender ? PLACEHOLDER_VERSION : packageVersion;
|
||||||
? PLACEHOLDER_VERSION
|
|
||||||
: packageVersion;
|
|
||||||
|
|
||||||
// Helpers
|
// Helpers
|
||||||
function updateCargoToml(content: string): string {
|
function updateCargoToml(content: string): string {
|
||||||
|
|
@ -34,10 +30,7 @@ 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(
|
return content.replace(regex, `version = "${targetVersion}"`);
|
||||||
regex,
|
|
||||||
`version = "${targetVersion}"`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateTauriConfig(content: string): string {
|
function updateTauriConfig(content: string): string {
|
||||||
|
|
@ -47,10 +40,7 @@ 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(
|
return content.replace(regex, `"version": "${targetVersion}"`);
|
||||||
regex,
|
|
||||||
`"version": "${targetVersion}"`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update Cargo.toml
|
// Update Cargo.toml
|
||||||
|
|
|
||||||
34
apps/web/src/components/modals/profile.tsx
Normal file
34
apps/web/src/components/modals/profile.tsx
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
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,7 +1,8 @@
|
||||||
import { Button } from "@tensamin/ui";
|
import { Button, Popover, PopoverContent, PopoverTrigger } from "@tensamin/ui";
|
||||||
import {
|
import {
|
||||||
ArrowLeft,
|
ArrowLeft,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
|
ChevronUp,
|
||||||
House,
|
House,
|
||||||
Phone,
|
Phone,
|
||||||
Settings,
|
Settings,
|
||||||
|
|
@ -17,6 +18,7 @@ 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();
|
||||||
|
|
@ -33,6 +35,8 @@ 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
|
||||||
|
|
@ -69,6 +73,10 @@ 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"
|
||||||
|
|
@ -77,8 +85,15 @@ 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>
|
||||||
<ChevronDown />
|
{userInfoOpen ? <ChevronUp /> : <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" />}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
1
bun.lock
1
bun.lock
|
|
@ -171,6 +171,7 @@
|
||||||
"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,7 +295,9 @@ async function updateLocalParticipantAttributes(
|
||||||
screenSharePreviewLength: attributes.screenSharePreview?.length ?? 0,
|
screenSharePreviewLength: attributes.screenSharePreview?.length ?? 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
await getRoom().localParticipant.setAttributes(attributes).catch((error) => {
|
await getRoom()
|
||||||
|
.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,
|
||||||
|
|
@ -795,7 +797,9 @@ export async function connect(callId: string) {
|
||||||
|
|
||||||
syncAllRemoteTrackSubscriptions();
|
syncAllRemoteTrackSubscriptions();
|
||||||
|
|
||||||
await getRoom().localParticipant.setMicrophoneEnabled(true).catch((error) => {
|
await getRoom()
|
||||||
|
.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;
|
||||||
|
|
|
||||||
|
|
@ -97,6 +97,7 @@ 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..."
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@
|
||||||
"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,16 +10,17 @@ 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);
|
||||||
|
|
||||||
function reduceDisplay(display: string) {
|
async function requestNotificationPermission() {
|
||||||
const words = display.split(" ");
|
if (!("Notification" in window)) return false;
|
||||||
if (words.length === 1) {
|
|
||||||
return display.slice(0, 2).toUpperCase();
|
if (Notification.permission === "granted") return true;
|
||||||
} else {
|
|
||||||
return words[0].charAt(0).toUpperCase() + words[1].charAt(0).toUpperCase();
|
const permission = await Notification.requestPermission();
|
||||||
}
|
return permission === "granted";
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Provider(props: { children: React.ReactNode }) {
|
export default function Provider(props: { children: React.ReactNode }) {
|
||||||
|
|
@ -29,6 +30,7 @@ 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) => {
|
||||||
|
|
@ -54,7 +56,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
|
||||||
) {
|
) {
|
||||||
await send(
|
void send(
|
||||||
"message_state",
|
"message_state",
|
||||||
{
|
{
|
||||||
message_state: "read",
|
message_state: "read",
|
||||||
|
|
@ -64,7 +66,7 @@ export default function Provider(props: { children: React.ReactNode }) {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
await send(
|
void send(
|
||||||
"message_state",
|
"message_state",
|
||||||
{
|
{
|
||||||
message_state: "received",
|
message_state: "received",
|
||||||
|
|
@ -91,6 +93,26 @@ 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: {
|
||||||
|
|
@ -100,12 +122,15 @@ export default function Provider(props: { children: React.ReactNode }) {
|
||||||
icon: (
|
icon: (
|
||||||
<Avatar>
|
<Avatar>
|
||||||
<AvatarImage src={user.avatar} />
|
<AvatarImage src={user.avatar} />
|
||||||
<AvatarFallback>{reduceDisplay(user.display)}</AvatarFallback>
|
<AvatarFallback>
|
||||||
|
{user.display.slice(0, 2).toUpperCase()}
|
||||||
|
</AvatarFallback>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}, [
|
}, [
|
||||||
subscribePush,
|
subscribePush,
|
||||||
|
|
@ -117,6 +142,7 @@ 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(() => {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue