This commit is contained in:
rosetta
2026-01-30 05:01:05 +02:00
commit 83f38dc63f
327 changed files with 18725 additions and 0 deletions

BIN
app/views/.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -0,0 +1,53 @@
import { Breadcrumbs } from "@/app/components/Breadcrumbs/Breadcrumbs";
import { InternalScreen } from "@/app/components/InternalScreen/InternalScreen";
import { SettingsAlert } from "@/app/components/SettingsAlert/SettingsAlert";
import { SettingsInput } from "@/app/components/SettingsInput/SettingsInput";
import { TextChain } from "@/app/components/TextChain/TextChain";
import { decodeWithPassword } from "@/app/crypto/crypto";
import { useAccount } from "@/app/providers/AccountProvider/useAccount";
import { Paper, Text } from "@mantine/core";
import { useState } from "react";
export function Backup() {
const [show, setShow] = useState("");
const [account] = useAccount();
const onChange = async (v : string) => {
try{
const decodedPhrase = await decodeWithPassword(v, account.seedPhraseEncrypted);
setShow(decodedPhrase);
}catch(e){
}
}
return (
<>
<Breadcrumbs text="Backup"></Breadcrumbs>
<InternalScreen>
<SettingsAlert text={
"If you want to give your recovery phrase to someone, please stop! This may lead to the compromise of your conversations."
}></SettingsAlert>
<SettingsInput.Default
hit="Confirmation"
type="password"
disabled={show.trim() !== ""}
mt={'sm'}
onChange={(e) => onChange(e.target.value)} placeholder="Enter confirmation"></SettingsInput.Default>
<Text fz={10} mt={3} c={'gray'} pl={'xs'} pr={'xs'}>
To view your recovery phrase, enter the password you specified when creating your account or restoring from a seed phrase.
</Text>
{show.trim() !== "" && (
<>
<Paper mt={'sm'} p={'md'} withBorder>
<TextChain rainbow={true} mt={'sm'} text={show}></TextChain>
</Paper>
<Text fz={10} mt={3} c={'gray'} pl={'xs'} pr={'xs'}>
Please don't share your seed phrase! The administration will never ask you for it.
</Text>
</>
)}
</InternalScreen>
</>
)
}

42
app/views/Chat/Chat.tsx Normal file
View File

@@ -0,0 +1,42 @@
import { ChatHeader } from "@/app/components/ChatHeader/ChatHeader";
import { DialogInput } from "@/app/components/DialogInput/DialogInput";
import { Messages } from "@/app/components/Messages/Messages";
import { DialogProvider } from "@/app/providers/DialogProvider/DialogProvider";
import { Flex } from "@mantine/core";
import { useParams } from "react-router-dom";
import { useRosettaBreakpoints } from "@/app/hooks/useRosettaBreakpoints";
import { useEffect } from "react";
import { useViewPanelsState, ViewPanelsState } from "@/app/hooks/useViewPanelsState";
import { GroupHeader } from "@/app/components/GroupHeader/GroupHeader";
import { useGroups } from "@/app/providers/DialogProvider/useGroups";
export function Chat() {
const params = useParams();
const dialog = params.id || "DELETED";
const {lg} = useRosettaBreakpoints();
const [__, setViewState] = useViewPanelsState();
const {hasGroup} = useGroups();
useEffect(() => {
if(!lg){
setViewState(ViewPanelsState.DIALOGS_PANEL_HIDE);
return;
}
if(lg){
setViewState(ViewPanelsState.DIALOGS_PANEL_SHOW);
}
}, [lg]);
return (<>
<DialogProvider dialog={dialog} key={dialog}>
<Flex direction={'column'} justify={'space-between'} h={'100%'}>
{/* Group Header */}
{hasGroup(dialog) && <GroupHeader></GroupHeader>}
{/* Dialog peer to peer Header */}
{!hasGroup(dialog) && <ChatHeader></ChatHeader>}
<Messages></Messages>
<DialogInput key={dialog}></DialogInput>
</Flex>
</DialogProvider>
</>);
}

View File

@@ -0,0 +1,59 @@
import { Button, Flex, Group, Text, Transition } from "@mantine/core";
import { useNavigate } from "react-router-dom";
import { InputChainHidden } from "@/app/components/InputChainHidden/InputChainHidden";
import { useMemory } from "@/app/providers/MemoryProvider/useMemory";
import { AuthFlowBreadcrumbs } from "@/app/components/AuthFlowBreadcrumbs/AuthFlowBreadcrumbs";
import { useEffect, useState } from "react";
export function ConfirmSeed() {
const navigate = useNavigate();
const [phrase, _] = useMemory("seed-phrase", "", true);
const [mounted, setMounted] = useState(false);
useEffect(() => {
if(phrase.trim() == ''){
navigate('/create-seed');
}
setTimeout(() => setMounted(true), 100);
}, [phrase]);
const onPassed = () => {
navigate('/set-password');
};
return (
<Flex h={520} w={385}>
<Flex h={'100%'} w={'100%'}>
<Flex h={'100%'} w={'100%'} direction="column" align="center" justify="space-between">
<AuthFlowBreadcrumbs rightSection={
<Button p={0} onClick={onPassed} variant={'transparent'}>Skip</Button>
} title="Confirm phrase"></AuthFlowBreadcrumbs>
<Flex>
<Transition mounted={mounted} transition="slide-up" duration={400} timingFunction="ease">
{(styles) => (
<Flex w={'100%'} direction={'column'} mt={'sm'} pl={'sm'} pr={'sm'} align={'center'} style={styles}>
<Text c="dimmed" ta={'center'} size={'xs'}>
To confirm that you have saved your seed phrase, please <strong>enter the missing words</strong>.
</Text>
</Flex>
)}
</Transition>
</Flex>
<Group p={'md'}>
<InputChainHidden w={120} size={'sm'} onPassed={onPassed} text={phrase} hidden={3}></InputChainHidden>
</Group>
<Flex align={'center'} w={'100%'} p={'md'} direction={'column'} justify={'center'} gap={'sm'}>
<Transition mounted={mounted} transition="slide-up" duration={500} timingFunction="ease">
{(styles) => (
<Flex p={'sm'} direction={'column'} gap={'lg'} style={styles}>
{/* Button section */}
</Flex>
)}
</Transition>
</Flex>
</Flex>
</Flex>
</Flex>
);
}

View File

@@ -0,0 +1,59 @@
import { Breadcrumbs } from "@/app/components/Breadcrumbs/Breadcrumbs";
import { InternalScreen } from "@/app/components/InternalScreen/InternalScreen";
import { SettingsInput } from "@/app/components/SettingsInput/SettingsInput";
import { Flex, Text } from "@mantine/core";
import { IconChevronRight } from "@tabler/icons-react";
import { useState } from "react";
import animationData from './users.json';
import Lottie from "lottie-react";
import { AnimatedButton } from "@/app/components/AnimatedButton/AnimatedButton";
import { useGroups } from "@/app/providers/DialogProvider/useGroups";
export function CreateGroup() {
const [title, setTitle] = useState<string>("");
const [description, setDescription] = useState<string>("");
const {createGroup, loading} = useGroups();
return (
<>
<Breadcrumbs text="Create group"></Breadcrumbs>
<InternalScreen>
<Flex align={'center'} direction={'column'} justify={'center'} gap={8} mb={'md'}>
<Lottie
style={{ width: 80, height: 80 }}
animationData={animationData}
loop={true}
></Lottie>
<Text fz={20} fw={600}>
Create group chat
</Text>
</Flex>
<SettingsInput.Default
hit="Group title"
placeholder="ex. Family Chat"
value={title}
onChange={(e) => setTitle(e.target.value)}
></SettingsInput.Default>
<Text fz={10} mt={3} c={'gray'} pl={'xs'} pr={'xs'}>
Group title, e.g., "Family Chat", helps members identify the group easily. <strong>Title cannot be changed after creation.</strong>
</Text>
<SettingsInput.Default
hit="Description"
mt={'sm'}
value={description}
placeholder="ex. A group for family members"
onChange={(e) => setDescription(e.target.value)}
></SettingsInput.Default>
<Text fz={10} mt={3} c={'gray'} pl={'xs'} pr={'xs'}>
Group description, e.g., "A group for family members", helps members understand the purpose of the group. Not required.
</Text>
<AnimatedButton rightSection={
<IconChevronRight size={14}></IconChevronRight>
} mt={'lg'} fullWidth animated={[
'#0078ff',
'#2ea6ff'
]} onClick={() => createGroup(title, description)} loading={loading}>Create</AnimatedButton>
</InternalScreen>
</>
);
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,95 @@
import { IconChevronRight, } from '@tabler/icons-react';
import { Flex, Text, Transition } from '@mantine/core';
import { TextChain } from '@/app/components/TextChain/TextChain';
import { useEffect, useState } from 'react';
import { generateMnemonic } from 'web-bip39';
import wordlist from 'web-bip39/wordlists/english'
import { CopyButtonIcon } from '@/app/components/CopyButtonIcon/CopyButtonIcon';
import { modals } from '@mantine/modals';
import { useClipboard } from '@mantine/hooks';
import { useNavigate } from 'react-router-dom';
import { useMemory } from '@/app/providers/MemoryProvider/useMemory';
import useWindow from '@/app/hooks/useWindow';
import { AnimatedButton } from '@/app/components/AnimatedButton/AnimatedButton';
import { AuthFlowBreadcrumbs } from '@/app/components/AuthFlowBreadcrumbs/AuthFlowBreadcrumbs';
export function CreateSeed() {
const [phrase, setPhrase] = useMemory("seed-phrase", "", true);
const {copy} = useClipboard();
const navigate = useNavigate();
const {setSize, setResizeble} = useWindow();
const [mounted, setMounted] = useState(false);
useEffect(() => {
setSize(385, 555);
setResizeble(false);
setTimeout(() => setMounted(true), 100);
}, []);
const openCopyModal = () => {
modals.openConfirmModal({
title: 'Stop',
centered: true,
children: (
<Text size="sm">
Do you really just want to copy your secret phrase and that's it?
We still recommend writing it down on a piece of paper rather than just copying it.
</Text>
),
withCloseButton: false,
labels: { confirm: 'Copy', cancel: "Cancel" },
confirmProps: { color: 'red' },
onCancel: () => copy("")
});
}
const generate = async () => {
const mnemonic = await generateMnemonic(wordlist);
setPhrase(mnemonic);
}
useEffect(() => {
generate();
}, []);
return (
<Flex w={'100%'} h={520}>
<Flex w={385} h={'100%'} direction={'column'} justify={'space-between'}>
<AuthFlowBreadcrumbs title="Create account"></AuthFlowBreadcrumbs>
<Transition mounted={mounted} transition="slide-up" duration={400} timingFunction="ease">
{(styles) => (
<Flex w={'100%'} direction={'column'} pl={'sm'} pr={'sm'} align={'center'} style={styles}>
<Text c="dimmed" ta={'center'} size={'xs'}>
This seed phrase is needed to access your correspondence, save it in a safe place
</Text>
</Flex>
)}
</Transition>
<Transition mounted={mounted} transition="slide-up" duration={500} timingFunction="ease">
{(styles) => (
<Flex p={'sm'} justify={'center'} align={'center'} style={styles}>
<TextChain text={phrase}></TextChain>
</Flex>
)}
</Transition>
<Transition mounted={mounted} transition="slide-up" duration={600} timingFunction="ease">
{(styles) => (
<Flex p={'sm'} direction={'column'} gap={'lg'} style={styles}>
<CopyButtonIcon onClick={openCopyModal} fullWidth size={'md'} value={phrase} caption='Copy phrase'></CopyButtonIcon>
<AnimatedButton animated={
['#2DA5FF', '#87DBFF']
} rightSection={
<IconChevronRight size={14}></IconChevronRight>
} fullWidth onClick={() => navigate('/confirm-seed')} size={'md'}>
Next step
</AnimatedButton>
</Flex>
)}
</Transition>
</Flex>
</Flex>
);
}

View File

@@ -0,0 +1,59 @@
import { PrivateView } from "@/app/components/PrivateView/PrivateView";
import { ProtocolState } from "@/app/providers/ProtocolProvider/ProtocolProvider";
import { useProtocolState } from "@/app/providers/ProtocolProvider/useProtocolState";
import { Flex, Text } from "@mantine/core";
import Lottie from "lottie-react";
import { useEffect } from "react";
import { useNavigate } from "react-router-dom";
import animationData from './inbox.json'
import { AnimatedButton } from "@/app/components/AnimatedButton/AnimatedButton";
import { useLogout } from "@/app/providers/AccountProvider/useLogout";
export function DeviceConfirm() {
const protocolState = useProtocolState();
const navigate = useNavigate();
const logout = useLogout();
useEffect(() => {
if(protocolState == ProtocolState.CONNECTED) {
navigate('/main');
}
}, [protocolState]);
return (
<PrivateView>
<Flex h={500} justify={'space-between'} align={'center'} direction={'column'}>
<Flex maw={380} direction={'column'} align={'center'} mt={'xl'}>
<Flex>
<Lottie style={{
width: 140,
height: 140
}} animationData={animationData}></Lottie>
</Flex>
<Flex mt={'xl'}>
<Text fw={500} fz={18}>
Confirm new device
</Text>
</Flex>
<Flex mt={'sm'} px={'lg'} justify={'center'}>
<Text ta={'center'} c={'dimmed'} fz={14}>
To confirm this device, please check your first device attached to your account and approve the new device.
</Text>
</Flex>
<Flex mt={'xl'} px={'lg'} w={'100%'} justify={'center'}>
<AnimatedButton onClick={() => {
logout();
}} variant="light" color="white" radius={'xl'} fullWidth animated={['#e03131', '#ff5656']}>Exit</AnimatedButton>
</Flex>
</Flex>
<Flex justify={'center'} mt={'xl'} px={'lg'} align={'center'}>
<Flex justify={'center'} gap={'sm'} align={'center'}>
<Text ta={'center'} c={'dimmed'} fz={12}>
Confirm device <strong>{window.deviceName}</strong> on your first device to loading your chats.
</Text>
</Flex>
</Flex>
</Flex>
</PrivateView>
)
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,11 @@
import { Box, Flex, Text } from "@mantine/core";
export function DialogPreview() {
return (<>
<Flex h={'100%'} align={'center'} justify={'center'}>
<Box p={'lg'}>
<Text size={'xs'} c={'gray'} fw={500}>Select dialog</Text>
</Box>
</Flex>
</>);
}

View File

@@ -0,0 +1,12 @@
import { DialogsPanel } from "@/app/components/DialogsPanel/DialogsPanel";
import { useRosettaBreakpoints } from "@/app/hooks/useRosettaBreakpoints";
import { Navigate } from "react-router-dom";
export function Dialogs() {
const {lg} = useRosettaBreakpoints();
return (
<>
{lg ? <Navigate to={'/main'}></Navigate> : <DialogsPanel></DialogsPanel>}
</>
)
}

View File

@@ -0,0 +1,27 @@
.inner {
display: flex;
justify-content: center;
align-items: center;
padding-top: calc(var(--mantine-spacing-xl));
}
.content {
max-width: 480px;
}
.title {
color: light-dark(var(--mantine-color-black), var(--mantine-color-white));
font-family:
Greycliff CF,
var(--mantine-font-family);
font-size: 44px;
line-height: 1.2;
font-weight: 900;
}
.highlight {
position: relative;
background-color: var(--mantine-color-blue-light);
border-radius: var(--mantine-radius-sm);
padding: 4px 12px;
}

View File

@@ -0,0 +1,82 @@
import { useMemory } from "@/app/providers/MemoryProvider/useMemory";
import { Flex, Text, Transition } from "@mantine/core";
import { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import { IconChevronRight } from "@tabler/icons-react";
import { InputChainWords } from "@/app/components/InputChainWords/InputChainWords";
import wordlist from "web-bip39/wordlists/english";
import useWindow from "@/app/hooks/useWindow";
import { AnimatedButton } from "@/app/components/AnimatedButton/AnimatedButton";
import { AuthFlowBreadcrumbs } from "@/app/components/AuthFlowBreadcrumbs/AuthFlowBreadcrumbs";
export function ExistsSeed() {
const navigate = useNavigate();
const [_, setPhrase] = useMemory("seed-phrase", "", true);
const [passed, setPassed] = useState(false);
const [words, setWords] = useState("");
const {setSize, setResizeble} = useWindow();
const [mounted, setMounted] = useState(false);
useEffect(() => {
setSize(385, 555);
setResizeble(false);
setTimeout(() => setMounted(true), 100);
}, []);
const onPassed = (words : string[]) => {
setWords(words.join(" "));
setPassed(true);
};
const onNotPassed = () => {
setPassed(false);
};
const onNextStep = () => {
setPhrase(words);
navigate('/set-password');
}
return (
<Flex w={'100%'} h={520}>
<Flex w={385} h={'100%'} direction={'column'} justify={'space-between'}>
<AuthFlowBreadcrumbs title="Import account"></AuthFlowBreadcrumbs>
<Transition mounted={mounted} transition="slide-up" duration={400} timingFunction="ease">
{(styles) => (
<Flex w={'100%'} direction={'column'} pl={'sm'} pr={'sm'} align={'center'} style={styles}>
<Text c="dimmed" ta={'center'} size={'xs'}>
Enter your seed phrase that you generated or that was created when setting up your account
</Text>
</Flex>
)}
</Transition>
<Transition mounted={mounted} transition="slide-up" duration={500} timingFunction="ease">
{(styles) => (
<Flex p={'sm'} justify={'center'} align={'center'} style={styles}>
<InputChainWords placeholderFunc={(i) => i + 1 + "."} wordlist={wordlist} onPassed={onPassed} onNotPassed={onNotPassed} words={12}></InputChainWords>
</Flex>
)}
</Transition>
<Transition mounted={mounted} transition="slide-up" duration={600} timingFunction="ease">
{(styles) => (
<Flex p={'sm'} direction={'column'} gap={'lg'} style={styles}>
<AnimatedButton
animated={['#2DA5FF', '#87DBFF']}
rightSection={<IconChevronRight size={14}></IconChevronRight>}
fullWidth
disabled={!passed}
onClick={() => onNextStep()}
size={'md'}
>
Next step
</AnimatedButton>
</Flex>
)}
</Transition>
</Flex>
</Flex>
);
}

View File

@@ -0,0 +1,74 @@
import { Breadcrumbs } from "@/app/components/Breadcrumbs/Breadcrumbs";
import { InternalScreen } from "@/app/components/InternalScreen/InternalScreen";
import { KeyImage } from "@/app/components/KeyImage/KeyImage";
import { useRosettaColors } from "@/app/hooks/useRosettaColors";
import { Button, Flex, Text, Tooltip, useMantineTheme } from "@mantine/core";
import { IconChevronLeft, IconLock } from "@tabler/icons-react";
import { useNavigate, useParams } from "react-router-dom";
export function GroupEncryption() {
const params = useParams();
const encryptKey = params.key!;
const theme = useMantineTheme();
const colors = useRosettaColors();
const navigate = useNavigate();
const translateStringToHex = (str: string) => {
return str.split('').map(c => {
/*
XOR for prevent share keys
*/
const hex = (c.charCodeAt(0) ^ 27).toString(16);
return hex.length === 1 ? '0' + hex : hex;
}).join(' ');
}
return (<>
<Breadcrumbs text={"Encryption key"}></Breadcrumbs>
<InternalScreen>
<Flex align={'center'} justify={'center'}>
<KeyImage radius={0} colors={[
theme.colors.blue[1],
theme.colors.blue[2],
theme.colors.blue[3],
theme.colors.blue[4],
theme.colors.blue[5]
]} size={180} keyRender={encryptKey}></KeyImage>
</Flex>
<Flex align={'center'} style={{
userSelect: 'none'
}} direction={'column'} mt={'sm'} justify={'center'}>
<Text ta="center" c={'dimmed'} ff={'monospace'} fz={'xs'} fw={500}>{
translateStringToHex(encryptKey.substring(0, 16))
}</Text>
<Text ta="center" c={'dimmed'} ff={'monospace'} fz={'xs'} fw={500}>{
translateStringToHex(encryptKey.substring(16, 32))
}</Text>
<Text ta="center" c={'dimmed'} ff={'monospace'} fz={'xs'} fw={500}>{
translateStringToHex(encryptKey.substring(32, 48))
}</Text>
<Text ta="center" c={'dimmed'} ff={'monospace'} fz={'xs'} fw={500}>{
translateStringToHex(encryptKey.substring(48, 64))
}</Text>
</Flex>
<Flex align={'center'} direction={'column'} mt={'sm'}>
<Flex direction={'row'} gap={5} align={'center'}>
<Text fw={500} fz={'sm'}>
Your messages is secure
</Text>
<Tooltip withArrow label="Secure and store only on your device.">
<IconLock size={16} color={colors.success}></IconLock>
</Tooltip>
</Flex>
<Text ta="center" maw={350} mt={'sm'} c={'dimmed'} fz={'xs'}>
This key is used to encrypt and decrypt messages. Your messages is secure and not stored on our servers.
</Text>
</Flex>
<Flex>
<Button onClick={() => navigate(-1)} leftSection={
<IconChevronLeft size={16}></IconChevronLeft>
} fullWidth mt={'md'} variant={'subtle'} color="blue">Back</Button>
</Flex>
</InternalScreen>
</>)
}

View File

@@ -0,0 +1,168 @@
import { ActionAvatar } from "@/app/components/ActionAvatar/ActionAvatar";
import { Breadcrumbs } from "@/app/components/Breadcrumbs/Breadcrumbs";
import { GroupInvite } from "@/app/components/GroupInvite/GroupInvite";
import { InternalScreen } from "@/app/components/InternalScreen/InternalScreen";
import { KeyImage } from "@/app/components/KeyImage/KeyImage";
import { SettingsInput } from "@/app/components/SettingsInput/SettingsInput";
import { SettingsPaper } from "@/app/components/SettingsPaper/SettingsPaper";
import { SettingsText } from "@/app/components/SettingsText/SettingsText";
import { SettingsTitle } from "@/app/components/SettingsTitle/SettingsTitle";
import { UsersTable } from "@/app/components/UsersTable/UsersTable";
import { VerifiedBadge } from "@/app/components/VerifiedBadge/VerifiedBadge";
import { useRosettaColors } from "@/app/hooks/useRosettaColors";
import { usePublicKey } from "@/app/providers/AccountProvider/usePublicKey";
import { useGroupMembers } from "@/app/providers/InformationProvider/useGroupMembers";
import { useGroups } from "@/app/providers/DialogProvider/useGroups";
import { useGroupInformation } from "@/app/providers/InformationProvider/useGroupInformation";
import { Button, Flex, Paper, Skeleton, Text, useMantineTheme } from "@mantine/core";
import { IconBan, IconDoorExit, IconMessage } from "@tabler/icons-react";
import { useEffect, useState } from "react";
import { useNavigate, useParams } from "react-router-dom";
import { modals } from "@mantine/modals";
export function GroupInfo() {
const params = useParams();
const {groupInfo} = useGroupInformation(params.id!);
const {members, loading: membersLoading} = useGroupMembers(params.id!, true);
const publicKey = usePublicKey();
const isAdmin = members.length > 0 && members[0] == publicKey;
const colors = useRosettaColors();
const navigate = useNavigate();
const [groupKey, setGroupKey] = useState<string>('');
const {getGroupKey, leaveGroup, banUserOnGroup, loading, getPrefix} = useGroups();
const theme = useMantineTheme();
useEffect(() => {
initGroupKey();
}, [params.id]);
const initGroupKey = async () => {
const key = await getGroupKey(groupInfo.groupId);
setGroupKey(key);
}
const leaveGroupWithModal = () => {
modals.openConfirmModal({
title: 'Leave group',
centered: true,
children: (
<Text size="sm">
You are attempting to leave the group. Are you sure you want to proceed?
</Text>
),
withCloseButton: false,
labels: { confirm: 'Leave', cancel: "Cancel" },
confirmProps: { color: 'red' },
onConfirm: () => {
leaveGroup(groupInfo.groupId);
}
});
}
const banUserWithModal = (userPublicKey: string) => {
modals.openConfirmModal({
title: 'Ban user from group',
centered: true,
children: (
<Text size="sm">
You are attempting to ban this user from the group.
</Text>
),
withCloseButton: false,
labels: { confirm: 'Ban', cancel: "Cancel" },
confirmProps: { color: 'red' },
onConfirm: () => {
confirmBan(userPublicKey);
}
});
}
const confirmBan = (userPublicKey: string) => {
banUserOnGroup(userPublicKey, groupInfo.groupId);
}
return (
<>
<Breadcrumbs text={groupInfo.title} rightSection={
<Button loading={loading} color={'red'} leftSection={<IconDoorExit size={15} />} onClick={leaveGroupWithModal} p={0} pr={6} variant={'transparent'}>Leave</Button>
}></Breadcrumbs>
<InternalScreen>
{(members.length > 0 || !membersLoading) && (
<>
<Paper radius="md" p="lg" bg={'transparent'}>
<ActionAvatar
forceChangeable={isAdmin}
title={groupInfo.title}
publicKey={getPrefix() + groupInfo.groupId} />
<Flex align={'center'} mt={'md'} justify={'center'} gap={5}>
<Text ta="center" fz="lg" fw={500}>{groupInfo.title.trim()}</Text>
</Flex>
<Text ta="center" c="dimmed" fz="sm">
{members.length} member{members.length !== 1 ? 's' : ''}
</Text>
</Paper>
{groupInfo.description.trim().length > 0 && (
<SettingsPaper p="xs">
<Text fw={500} pl={'xs'} fz={'xs'} c={'blue'}>Information</Text>
<Text pl={'xs'} fz={12} mt={3} c={'dimmed'}>
{groupInfo.description.trim()}
</Text>
</SettingsPaper>
)}
{isAdmin && (
<GroupInvite groupId={groupInfo.groupId} mt={'sm'}></GroupInvite>
)}
<SettingsInput.Clickable mt={'sm'} onClick={() => navigate('/main/groupencrypt/' + groupKey)} hit="Encryption key" settingsIcon={
<KeyImage radius={0} colors={[
theme.colors.blue[1],
theme.colors.blue[2],
theme.colors.blue[3],
theme.colors.blue[4],
theme.colors.blue[5]
]} size={18} keyRender={groupKey}></KeyImage>
}></SettingsInput.Clickable>
<SettingsText>Click to see encryption key for secure group communication</SettingsText>
{members.length > 0 && isAdmin && (
<>
<SettingsTitle mt={'sm'}>Group Members</SettingsTitle>
<UsersTable rightSection={(userPublicKey: string) => (
<>
<IconMessage onClick={() => navigate('/main/chat/' + userPublicKey)} stroke={1.4} size={20} color={colors.brandColor}></IconMessage>
{publicKey == userPublicKey && (
<IconDoorExit onClick={leaveGroupWithModal} stroke={1.4} size={20} color={colors.error}></IconDoorExit>
)}
{publicKey != userPublicKey && isAdmin && (
<IconBan onClick={() => banUserWithModal(userPublicKey)} stroke={1.4} size={20} color={colors.error}></IconBan>
)}
</>
)} usersPublicKeys={members} />
<SettingsText>
Members are listed in order of group creation. The first member is the group admin and can manage group settings.
</SettingsText>
</>
)}
{!isAdmin && (
<Flex mt={'lg'} gap={5} align={'center'}>
<Text fz={10} pl={'xs'} c={'gray'}>
Group administrator has marked in messages with
</Text>
<VerifiedBadge size={17} color={'gold'} verified={3}></VerifiedBadge>
</Flex>
)}
</>
)}
{membersLoading && members.length <= 0 && (
<>
<Flex direction={'column'} w={'100%'}>
<Skeleton h={150}></Skeleton>
<Skeleton h={50} mt={'sm'}></Skeleton>
<Skeleton h={50} mt={'sm'}></Skeleton>
<Skeleton h={50} mt={'sm'}></Skeleton>
</Flex>
</>
)}
</InternalScreen>
</>
);
}

View File

@@ -0,0 +1,168 @@
.wrapper {
box-sizing: border-box;
background-color: light-dark(var(--mantine-color-white), var(--mantine-color-dark-8));
width: 100%;
height: 100%;
position: fixed;
top: 0;
left: 0;
display: flex;
align-items: center;
align-content: center;
justify-content: center;
overflow: auto;
padding: 60px 20px 30px;
}
.title {
font-family:
Greycliff CF,
var(--mantine-font-family);
font-size: 62px;
font-weight: 900;
line-height: 1.1;
margin-top: 70px;
padding: 0;
color: light-dark(var(--mantine-color-black), var(--mantine-color-white));
@media (max-width: $mantine-breakpoint-sm) {
font-size: 42px;
line-height: 1.2;
}
}
.description {
margin-top: var(--mantine-spacing-xl);
font-size: 24px;
@media (max-width: $mantine-breakpoint-sm) {
font-size: 18px;
}
}
.controls {
margin-top: calc(var(--mantine-spacing-xl) * 2);
@media (max-width: $mantine-breakpoint-sm) {
margin-top: var(--mantine-spacing-xl);
}
}
.control {
height: 54px;
padding-left: 38px;
padding-right: 38px;
@media (max-width: $mantine-breakpoint-sm) {
height: 54px;
padding-left: 18px;
padding-right: 18px;
flex: 1;
}
}
.carousel {
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
height: 340px;
}
.slideContent {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
flex: 1;
justify-content: flex-start;
animation: fadeInSlide 0.5s ease-in-out;
width: 100%;
}
@keyframes fadeInSlide {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.iconWrapper {
width: 160px;
height: 160px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 32px;
transition: all 0.5s ease;
animation: scaleIn 0.5s ease-in-out;
}
@keyframes scaleIn {
from {
opacity: 0;
transform: scale(0.8);
}
to {
opacity: 1;
transform: scale(1);
}
}
.title {
font-size: 24px;
font-weight: 600;
margin-bottom: 16px;
color: light-dark(var(--mantine-color-black), var(--mantine-color-white));
animation: fadeIn 0.5s ease-in-out 0.1s both;
min-height: 30px;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.description {
font-size: 15px;
color: light-dark(var(--mantine-color-gray-6), var(--mantine-color-dark-2));
line-height: 1.5;
max-width: 300px;
animation: fadeIn 0.5s ease-in-out 0.2s both;
min-height: 68px;
}
.dots {
margin: 20px 0;
gap: 8px;
}
.dot {
width: 8px;
height: 8px;
border-radius: 50%;
background-color: light-dark(var(--mantine-color-gray-4), var(--mantine-color-dark-4));
border: none;
cursor: pointer;
transition: all 0.3s ease;
padding: 0;
}
.dot:hover {
background-color: light-dark(var(--mantine-color-gray-5), var(--mantine-color-dark-3));
}
.dotActive {
width: 24px;
border-radius: 4px;
background-color: light-dark(var(--mantine-color-blue-6), var(--mantine-color-blue-4));
}

View File

@@ -0,0 +1,160 @@
import { useEffect, useState } from 'react';
import { Button, Group, Text, Flex } from '@mantine/core';
import { IconShieldLock, IconBolt, IconDeviceMobile } from '@tabler/icons-react';
import classes from './Introduction.module.css';
import { useNavigate } from 'react-router-dom';
import useWindow, { ElectronTheme } from '@/app/hooks/useWindow';
import { AnimatedButton } from '@/app/components/AnimatedButton/AnimatedButton';
const slides = [
{
title: 'Security',
description: 'Your data is protected by modern encryption and stays only on your device',
icon: IconShieldLock,
color: '#4CAF50',
},
{
title: 'Speed',
description: 'Instant synchronization and fast performance without delays',
icon: IconBolt,
color: '#FF9800',
},
{
title: 'Simple Interface',
description: 'Intuitive design that is clear at first glance',
icon: IconDeviceMobile,
color: '#2196F3',
},
];
export function Introduction() {
const navigate = useNavigate();
const {setSize, setResizeble, setTheme} = useWindow();
const [activeSlide, setActiveSlide] = useState(0);
const [touchStart, setTouchStart] = useState<number | null>(null);
const [touchEnd, setTouchEnd] = useState<number | null>(null);
// Минимальное расстояние свайпа (в px)
const minSwipeDistance = 50;
useEffect(() => {
setSize(385, 555);
setResizeble(false);
setTheme(ElectronTheme.SYSTEM);
}, []);
const handleGetStarted = () => {
navigate('/create-seed');
};
const goToSlide = (index: number) => {
setActiveSlide(index);
};
const nextSlide = () => {
setActiveSlide((prev) => (prev + 1) % slides.length);
};
const prevSlide = () => {
setActiveSlide((prev) => (prev - 1 + slides.length) % slides.length);
};
const onTouchStart = (e: React.TouchEvent) => {
setTouchEnd(null);
setTouchStart(e.targetTouches[0].clientX);
};
const onTouchMove = (e: React.TouchEvent) => {
setTouchEnd(e.targetTouches[0].clientX);
};
const onTouchEnd = () => {
if (!touchStart || !touchEnd) return;
const distance = touchStart - touchEnd;
const isLeftSwipe = distance > minSwipeDistance;
const isRightSwipe = distance < -minSwipeDistance;
if (isLeftSwipe) {
nextSlide();
} else if (isRightSwipe) {
prevSlide();
}
};
const handleCarouselClick = () => {
nextSlide();
};
const CurrentIcon = slides[activeSlide].icon;
return (
<div className={classes.wrapper}>
<Flex h={'100%'} w={'100%'} direction={'column'} justify={'space-between'} align={'center'}>
<div>
<div
className={classes.carousel}
onTouchStart={onTouchStart}
onTouchMove={onTouchMove}
onTouchEnd={onTouchEnd}
onClick={handleCarouselClick}
style={{ cursor: 'pointer', userSelect: 'none' }}
>
<div className={classes.slideContent} key={activeSlide}>
<div
className={classes.iconWrapper}
style={{ backgroundColor: `${slides[activeSlide].color}20` }}
>
<CurrentIcon
size={80}
stroke={1.5}
color={slides[activeSlide].color}
/>
</div>
<Text size={'sm'} className={classes.title} fw={500}>
{slides[activeSlide].title}
</Text>
<Text mt={'xs'} size={'xs'} c={'dimmed'} className={classes.description}>
{slides[activeSlide].description}
</Text>
</div>
<Group justify="center" className={classes.dots}>
{slides.map((_, index) => (
<button
key={index}
className={`${classes.dot} ${index === activeSlide ? classes.dotActive : ''}`}
onClick={(e) => {
e.stopPropagation();
goToSlide(index);
}}
aria-label={`Go to slide ${index + 1}`}
/>
))}
</Group>
</div>
</div>
<Flex gap={'xs'} direction={'column'} w={'100%'}>
<AnimatedButton
fullWidth
size="md"
animated={['#2DA5FF', '#87DBFF']}
onClick={handleGetStarted}
>
Get Started
</AnimatedButton>
<Button
fullWidth
variant={'transparent'}
size="md"
onClick={() => navigate('/exists-seed')}
>
Already have an account
</Button>
</Flex>
</Flex>
</div>
);
}

View File

@@ -0,0 +1,28 @@
.inner {
display: flex;
justify-content: center;
align-items: center;
padding-top: calc(var(--mantine-spacing-xl));
}
.content {
max-width: 480px;
}
.title {
color: light-dark(var(--mantine-color-black), var(--mantine-color-white));
font-family:
Greycliff CF,
var(--mantine-font-family);
font-size: 44px;
line-height: 1.2;
font-weight: 900;
}
.highlight {
position: relative;
background-color: var(--mantine-color-blue-light);
border-radius: var(--mantine-radius-sm);
padding: 4px 12px;
}

View File

@@ -0,0 +1,190 @@
import { Anchor, AspectRatio, Avatar, Box, Flex, PasswordInput, Skeleton, Text, Transition} from "@mantine/core";
import classes from './Lockscreen.module.css'
import { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import useWindow from "@/app/hooks/useWindow";
import { decodeWithPassword, generateHashFromPrivateKey } from "@/app/crypto/crypto";
import { useAccountProvider } from "@/app/providers/AccountProvider/useAccountProvider";
import { Account, AccountBase } from "@/app/providers/AccountProvider/AccountProvider";
import { useUserCache } from "@/app/providers/InformationProvider/useUserCache";
import { useRosettaColors } from "@/app/hooks/useRosettaColors";
import { IconArrowsExchange, IconFingerprint } from "@tabler/icons-react";
import { SIZE_LOGIN_WIDTH_PX } from "@/app/constants";
import { modals } from "@mantine/modals";
import { useAvatars } from "@/app/providers/AvatarProvider/useAvatars";
import { AnimatedButton } from "@/app/components/AnimatedButton/AnimatedButton";
import { DiceDropdown } from "@/app/components/DiceDropdown/DiceDropdown";
import { dotCenterIfNeeded } from "@/app/utils/utils";
export function Lockscreen() {
const [password, setPassword] = useState("");
const navigate = useNavigate();
const { setSize, setResizeble } = useWindow();
const { allAccounts, selectAccountToLoginDice, loginDiceAccount, loginAccount } = useAccountProvider();
const userInfo = useUserCache(loginDiceAccount.publicKey);
const [error, setError] = useState(false);
const colors = useRosettaColors();
const avatars = useAvatars(loginDiceAccount.publicKey);
const [mounted, setMounted] = useState(false);
useEffect(() => {
setSize(385, 555);
setResizeble(false);
setTimeout(() => setMounted(true), 100);
}, []);
useEffect(() => {
if (loginDiceAccount.publicKey == "" && allAccounts.length <= 0) {
navigate("/");
return;
}
}, [loginDiceAccount])
const onUnlockPressed = async () => {
try {
const decryptedHex = await decodeWithPassword(password, loginDiceAccount.privateKey);
const privateKeyHash = await generateHashFromPrivateKey(decryptedHex);
const account: Account = {
privateKey: loginDiceAccount.privateKey,
publicKey: loginDiceAccount.publicKey,
privateHash: privateKeyHash,
privatePlain: decryptedHex,
seedPhraseEncrypted: loginDiceAccount.seedPhraseEncrypted
};
loginAccount(account);
navigate("/main");
} catch (e) {
setError(true);
}
}
const createAccount = () => {
modals.openConfirmModal({
title: 'Create account',
centered: true,
children: (
<Text size="sm">
You may be create new account or import existing
</Text>
),
withCloseButton: false,
labels: { confirm: 'Create new', cancel: "Import" },
cancelProps: {
autoFocus: false,
style: {
outline: 'none'
}
},
onCancel: () => {
navigate("/exists-seed");
},
onConfirm: () => {
navigate("/create-seed");
}
});
}
const selectAccount = (account: AccountBase) => {
selectAccountToLoginDice(account);
}
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === "Enter") {
event.preventDefault();
onUnlockPressed();
}
};
window.addEventListener("keydown", handleKeyDown);
return () => {
window.removeEventListener("keydown", handleKeyDown);
};
}, [onUnlockPressed]);
return (
<Flex style={{
userSelect: 'none'
}} h={500} justify={'space-between'} direction={'column'} align={'center'}>
<div className={classes.inner}>
<div className={classes.content}>
<Flex mt={'md'} align={'center'} direction={'column'} justify={'center'}>
{userInfo && (
<Box mt={'sm'}>
<Flex align={'center'} gap={'sm'} direction={'column'}>
<AspectRatio ratio={2/2} maw={80} mx="auto" pos="relative">
<Avatar radius={'md'} src={avatars.length > 0 ? avatars[0].avatar : undefined} size={80} color={'initials'} name={userInfo.title}></Avatar>
</AspectRatio>
<Flex gap={8} justify={'center'} align={'center'}>
<Text fz={20} h={25} fw={'bold'}>{dotCenterIfNeeded(userInfo.title, 20, 5)}</Text>
<DiceDropdown selectedPublicKey={loginDiceAccount.publicKey} onClick={selectAccount}>
<IconArrowsExchange style={{
position: 'relative',
top: 4,
cursor: 'pointer'
}} color={colors.brandColor} size={18} />
</DiceDropdown>
</Flex>
<Text c={'dimmed'} mt={'xs'} ta={'center'} w={SIZE_LOGIN_WIDTH_PX} size={'xs'}>For unlock account enter password</Text>
</Flex>
</Box>
)}
{!userInfo && (
<Box>
<Flex align={'center'} gap={'sm'} direction={'column'} justify={'center'}>
<Skeleton w={80} h={80}></Skeleton>
<Skeleton h={25} w={150}></Skeleton>
</Flex>
</Box>
)}
</Flex>
<Transition mounted={mounted} transition="slide-up" duration={400} timingFunction="ease">
{(styles) => (
<Flex style={styles} mt={40} direction={'column'} align={'center'} justify={'center'} gap={'lg'}>
<PasswordInput
placeholder="Password"
value={password}
onChange={(e) => { setPassword(e.target.value); setError(false) }}
size="md"
w={SIZE_LOGIN_WIDTH_PX}
error={error && "Invalid password"}
styles={{
input: {
border: '0px solid ' + colors.borderColor,
backgroundColor: colors.mainColor
},
error: {
color: colors.error
}
}}
/>
<Flex w={SIZE_LOGIN_WIDTH_PX} align={'center'} direction={'column'} justify={'space-between'}>
<AnimatedButton fullWidth leftSection={
<IconFingerprint size={16} />
} animated={['#2DA5FF', '#87DBFF']} size={'md'} onClick={onUnlockPressed}>Enter</AnimatedButton>
</Flex>
</Flex>
)}
</Transition>
</div>
</div>
<Transition mounted={mounted} transition="slide-up" duration={500} timingFunction="ease">
{(styles) => (
<Flex style={styles}>
<Text c={'dimmed'} ta={'center'} w={SIZE_LOGIN_WIDTH_PX} size={'xs'}>
You can also <Anchor style={{
textDecoration: 'none'
}} fw={500} onClick={() => navigate('/exists-seed')}>recover your password</Anchor> or create a <Anchor style={{
textDecoration: 'none'
}} fw={500} onClick={createAccount}>new account.</Anchor>
</Text>
</Flex>
)}
</Transition>
</Flex>
)
}

View File

@@ -0,0 +1,5 @@
.dialogs_wrapper {
display: flex;
flex-direction: column;
overflow-y: scroll;
}

179
app/views/Main/Main.tsx Normal file
View File

@@ -0,0 +1,179 @@
import { Alert, Box, Button, Divider, Flex, Overlay } from "@mantine/core";
import { useEffect, useState } from "react";
import { Route, Routes, useLocation, useNavigate } from "react-router-dom";
import { Profile } from "@/app/views/Profile/Profile";
import { useRosettaColors } from "@/app/hooks/useRosettaColors";
import { DialogPreview } from "@/app/views/DialogPreview/DialogPreview";
import { Theme } from "../Theme/Theme";
import { Safety } from "../Safety/Safety";
import { Backup } from "../Backup/Backup";
import { Chat } from "../Chat/Chat";
import { PrivateView } from "@/app/components/PrivateView/PrivateView";
import { useDialogFiber } from "@/app/providers/DialogProvider/useDialogFiber";
import { Update } from "../Update/Update";
import { useRosettaBreakpoints } from "@/app/hooks/useRosettaBreakpoints";
import { Dialogs } from "../Dialogs/Dialogs";
import { DialogsPanel } from "@/app/components/DialogsPanel/DialogsPanel";
import { useViewPanelsState, ViewPanelsState } from "@/app/hooks/useViewPanelsState";
import { TransportProvider } from "@/app/providers/TransportProvider/TransportProvider";
import { useDebouncedCallback } from "@mantine/hooks";
import { SystemAccountProvider } from "@/app/providers/SystemAccountsProvider/SystemAccountsProvider";
import { CreateGroup } from "../CreateGroup/CreateGroup";
import { GroupInfo } from "../GroupInfo/GroupInfo";
import { GroupEncryption } from "../GroupEncryption/GroupEncryption";
import useWindow from "@/app/hooks/useWindow";
import { usePublicKey } from "@/app/providers/AccountProvider/usePublicKey";
import { useDatabase } from "@/app/providers/DatabaseProvider/useDatabase";
import { useMemoryClean } from "@/app/providers/MemoryProvider/useMemoryClean";
import { useAccountProvider } from "@/app/providers/AccountProvider/useAccountProvider";
import { useLogout } from "@/app/providers/AccountProvider/useLogout";
export function Main() {
const { mainColor, borderColor } = useRosettaColors();
const { lg } = useRosettaBreakpoints();
const location = useLocation();
const [viewState, setViewState] = useViewPanelsState();
useDialogFiber();
const { setSize, setResizeble } = useWindow();
/**
* Operation: OLD PUBLIC KEY MIGRATION
*/
const publicKey = usePublicKey();
const [oldPublicKey, setOldPublicKey] = useState(false);
const { runQuery } = useDatabase();
const navigate = useNavigate();
const memClean = useMemoryClean();
const { setAccounts } = useAccountProvider();
const logout = useLogout();
useEffect(() => {
if (publicKey.length > 80) {
/**
* Old public keys
*/
setOldPublicKey(true);
}
}, [publicKey]);
const dropAccountsAndMessages = async () => {
localStorage.clear();
await runQuery("DELETE FROM accounts");
await runQuery("DELETE FROM messages");
await runQuery("DELETE FROM dialogs");
memClean();
setAccounts([]);
logout();
setTimeout(() => {
navigate("/");
}, 500);
}
useEffect(() => {
let { width, height } = getWindowSavedSize();
setSize(width, height);
setResizeble(true);
const sizeListener = () => {
saveSize(window.innerWidth, window.innerHeight);
};
window.addEventListener("resize", sizeListener);
return () => {
window.removeEventListener("resize", sizeListener);
}
}, []);
useEffect(() => {
if (lg && viewState == ViewPanelsState.DIALOGS_PANEL_ONLY) {
setViewState(ViewPanelsState.DIALOGS_PANEL_SHOW);
return;
}
if (!lg && location.pathname != '/main') {
setViewState(ViewPanelsState.DIALOGS_PANEL_HIDE);
return;
}
if (!lg && location.pathname == '/main' && window.innerWidth < 770) {
setViewState(ViewPanelsState.DIALOGS_PANEL_ONLY);
return;
}
if (!lg) {
setViewState(ViewPanelsState.DIALOGS_PANEL_HIDE);
return;
}
if (lg) {
setViewState(ViewPanelsState.DIALOGS_PANEL_SHOW);
return;
}
}, [lg, location]);
const saveSize = useDebouncedCallback((w: number, h: number) => {
localStorage.setItem("windowWidth", w.toString());
localStorage.setItem("windowHeight", h.toString());
}, 1000);
const getWindowSavedSize = () => {
const savedWidth = localStorage.getItem("windowWidth");
const savedHeight = localStorage.getItem("windowHeight");
return {
width: savedWidth ? parseInt(savedWidth) : window.innerWidth,
height: savedHeight ? parseInt(savedHeight) : window.innerHeight
}
}
return (
<PrivateView>
<SystemAccountProvider>
<TransportProvider>
<Flex direction={'row'} style={{
height: '100%',
width: '100vw',
}}>
<div style={{
display: viewState != ViewPanelsState.DIALOGS_PANEL_HIDE ? 'block' : 'none',
width: viewState == ViewPanelsState.DIALOGS_PANEL_ONLY ? '100%' : '300px',
}}>
<DialogsPanel></DialogsPanel>
</div>
<Divider color={borderColor} orientation={'vertical'}></Divider>
{viewState != ViewPanelsState.DIALOGS_PANEL_ONLY && <Box
bg={mainColor}
style={{
flexGrow: 1,
height: 'calc(100vh - 27px)',
width: `calc(100% - 300px)`,
minWidth: 0
}}
>
<Routes>
<Route path={'/chat/:id'} element={<Chat />}></Route>
<Route path={'/profile/:id'} element={<Profile />}></Route>
<Route path={'/'} element={<DialogPreview />}></Route>
<Route path={'/theme'} element={<Theme />}></Route>
<Route path={'/safety'} element={<Safety />}></Route>
<Route path={'/update'} element={<Update />}></Route>
<Route path={'/backup'} element={<Backup />}></Route>
<Route path={'/dialogs'} element={<Dialogs />}></Route>
<Route path={'/newgroup'} element={<CreateGroup />}></Route>
<Route path={'/group/:id'} element={<GroupInfo />}></Route>
<Route path={'/groupencrypt/:key'} element={<GroupEncryption />}></Route>
</Routes>
</Box>}
</Flex>
{oldPublicKey && (
<Overlay blur={8} color="#333">
<Flex direction={'column'} align={'center'} justify={'center'} h={'100%'}>
<Alert w={400} variant="filled" color="red" title="Old account">
Your account uses an old format public key which is no longer supported. Please create a new account to continue using the application.
<br></br>After press "OK" button, the application will close and remove all data.
</Alert>
<Button w={400} mt={'md'} color="red" onClick={dropAccountsAndMessages}>OK</Button>
</Flex>
</Overlay>
)}
</TransportProvider>
</SystemAccountProvider>
</PrivateView>);
}

View File

@@ -0,0 +1,143 @@
import { ColorSwatch, Text, useComputedColorScheme } from "@mantine/core";
import { SettingsInput } from "@/app/components/SettingsInput/SettingsInput";
import { useState } from "react";
import { Breadcrumbs } from "@/app/components/Breadcrumbs/Breadcrumbs";
import { useNavigate } from "react-router-dom";
import { InternalScreen } from "@/app/components/InternalScreen/InternalScreen";
import { PacketUserInfo } from "@/app/providers/ProtocolProvider/protocol/packets/packet.userinfo";
import { PacketResult, ResultCode } from "@/app/providers/ProtocolProvider/protocol/packets/packet.result";
import { ProfileCard } from "@/app/components/ProfileCard/ProfileCard";
import { useUserInformation } from "@/app/providers/InformationProvider/useUserInformation";
import { OnlineState } from "@/app/providers/ProtocolProvider/protocol/packets/packet.onlinestate";
import { usePrivateKeyHash } from "@/app/providers/AccountProvider/usePrivateKeyHash";
import { usePublicKey } from "@/app/providers/AccountProvider/usePublicKey";
import { useSender } from "@/app/providers/ProtocolProvider/useSender";
import { usePacket } from "@/app/providers/ProtocolProvider/usePacket";
import { SettingsIcon } from "@/app/components/SettingsIcon/SettingsIcon";
import { IconBrush, IconHomeCog, IconLogout, IconRefresh } from "@tabler/icons-react";
import { useLogout } from "@/app/providers/AccountProvider/useLogout";
import { RosettaPower } from "@/app/components/RosettaPower/RosettaPower";
export function MyProfile() {
const publicKey = usePublicKey();
const [userInfo, updateUserInformation] = useUserInformation(publicKey);
const privateKey = usePrivateKeyHash();
const [title, setTitle] = useState(userInfo.title);
const [username, setUsername] = useState(userInfo.username);
const colorScheme = useComputedColorScheme();
const navigate = useNavigate();
const send = useSender();
const logout = useLogout();
const saveProfileData = () => {
let packet = new PacketUserInfo();
packet.setUsername(username);
packet.setTitle(title);
packet.setPrivateKey(privateKey);
send(packet);
}
usePacket(0x2, function (packet : PacketResult) {
switch (packet.getResultCode()) {
case ResultCode.SUCCESS:
updateUserInformation({
username: username,
title: title,
publicKey: publicKey,
online: OnlineState.OFFLINE,
verified: userInfo.verified
});
break;
}
}, [title, username]);
return (
<>
<Breadcrumbs onClick={saveProfileData} text="Profile"></Breadcrumbs>
<InternalScreen>
<ProfileCard
title={title}
publicKey={publicKey}
username={username}
verified={userInfo.verified}
></ProfileCard>
<SettingsInput.Group>
<SettingsInput.Default
hit="Your name"
placeholder="ex. Freddie Gibson"
value={title}
onChange={(e) => setTitle(e.target.value)}
></SettingsInput.Default>
<SettingsInput.Default
hit="Username"
value={username}
placeholder="ex. freddie871"
onChange={(e) => setUsername(e.target.value)}
></SettingsInput.Default>
</SettingsInput.Group>
<SettingsInput.Copy mt={'sm'} hit="Public Key" value={
publicKey
} placeholder="Public"></SettingsInput.Copy>
<Text fz={10} mt={3} c={'gray'} pl={'xs'} pr={'xs'}>
This is your public key. If you haven't set a @username yet, you can ask a friend to message you using your public key.
</Text>
<SettingsInput.Clickable
settingsIcon={
<SettingsIcon bg="var(--mantine-color-lime-6)"
icon={IconRefresh}>
</SettingsIcon>
}
hit="Updates"
onClick={() => navigate('/main/update')}
mt={'sm'}
></SettingsInput.Clickable>
<Text fz={10} mt={3} c={'gray'} pl={'xs'} pr={'xs'}>
You can check for new versions of the app here. Updates may include security improvements and new features.
</Text>
<SettingsInput.Clickable
settingsIcon={
<SettingsIcon bg={'indigo'}
icon={IconBrush}>
</SettingsIcon>
}
onClick={() => navigate('/main/theme')} mt={'sm'} hit="Theme" rightSection={
<ColorSwatch color={
colorScheme == 'light' ? '#000' : '#FFF'
} size={20}></ColorSwatch>
}></SettingsInput.Clickable>
<Text fz={10} mt={3} c={'gray'} pl={'xs'} pr={'xs'}>
You can change the theme.
</Text>
<SettingsInput.Clickable
settingsIcon={
<SettingsIcon bg="grape"
icon={IconHomeCog}>
</SettingsIcon>
}
hit="Safety"
onClick={() => navigate('/main/safety')}
mt={'sm'}
></SettingsInput.Clickable>
<Text fz={10} mt={3} c={'gray'} pl={'xs'} pr={'xs'}>
You can learn more about your safety on the safety page,
please make sure you are viewing the screen alone <strong>before proceeding to the safety page</strong>.
</Text>
<SettingsInput.Clickable
settingsIcon={
<SettingsIcon bg={'red'}
icon={IconLogout}>
</SettingsIcon>
}
hit={'Logout'}
mt={'sm'}
onClick={logout}
rightChevronHide
c={'red'}
></SettingsInput.Clickable>
<Text fz={10} mt={3} c={'gray'} pl={'xs'} pr={'xs'}>
Logging out of your account. After logging out, you will be redirected to the password entry page.
</Text>
<RosettaPower mt={'lg'}></RosettaPower>
</InternalScreen>
</>
);
}

View File

@@ -0,0 +1,68 @@
import { Breadcrumbs } from "@/app/components/Breadcrumbs/Breadcrumbs";
import { InternalScreen } from "@/app/components/InternalScreen/InternalScreen";
import { ProfileCard } from "@/app/components/ProfileCard/ProfileCard";
import { SettingsInput } from "@/app/components/SettingsInput/SettingsInput";
import { useBlacklist } from "@/app/providers/BlacklistProvider/useBlacklist";
import { useUserInformation } from "@/app/providers/InformationProvider/useUserInformation";
import { Text } from "@mantine/core";
interface OtherProfileProps {
publicKey: string;
}
export function OtherProfile(props : OtherProfileProps) {
const [userInfo] = useUserInformation(props.publicKey);
const [blocked, blockUser, unblockUser] = useBlacklist(userInfo.publicKey);
return (
<>
<Breadcrumbs text={userInfo.title}></Breadcrumbs>
<InternalScreen>
<ProfileCard
title={userInfo.title}
publicKey={userInfo.publicKey}
username={userInfo.username}
verified={userInfo.verified}
></ProfileCard>
{userInfo.username.trim() != "" && (
<>
<SettingsInput.Copy mt={'sm'} hit="Username" value={
userInfo.username
} placeholder="Public"></SettingsInput.Copy>
<Text fz={10} mt={3} c={'gray'} pl={'xs'} pr={'xs'}>
Username for search user or send message.
</Text>
</>
)}
<SettingsInput.Copy mt={'sm'} hit="Public Key" value={
userInfo.publicKey
} placeholder="Public"></SettingsInput.Copy>
<Text fz={10} mt={3} c={'gray'} pl={'xs'} pr={'xs'}>
This is user public key. If user haven't set a @username yet, you can send message using your public key.
</Text>
{blocked && (<>
<SettingsInput.Clickable mt={'sm'} c={'green'}
hit="Unblock user"
onClick={unblockUser}
rightChevronHide
>
</SettingsInput.Clickable>
<Text fz={10} mt={3} c={'gray'} pl={'xs'} pr={'xs'}>
If you want the user to be able to send you messages again, you can unblock them. You can block them later.
</Text>
</>)}
{!blocked && (<>
<SettingsInput.Clickable mt={'sm'} c={'red'}
hit="Block this user"
onClick={blockUser}
rightChevronHide
>
</SettingsInput.Clickable>
<Text fz={10} mt={3} c={'gray'} pl={'xs'} pr={'xs'}>
The person will no longer be able to message you if you block them. You can unblock them later.
</Text>
</>)}
</InternalScreen>
</>
);
}

View File

@@ -0,0 +1,9 @@
.icon {
color: light-dark(var(--mantine-color-gray-5), var(--mantine-color-dark-3));
}
.name {
font-family:
Greycliff CF,
var(--mantine-font-family);
}

View File

@@ -0,0 +1,16 @@
import { useParams } from "react-router-dom";
import { MyProfile } from "./MyProfile";
import { OtherProfile } from "./OtherProfile";
import { usePublicKey } from "@/app/providers/AccountProvider/usePublicKey";
export function Profile() {
const params = useParams();
const publicKey = usePublicKey();
return (
<>
{params.id == "me" || params.id == publicKey ? <MyProfile></MyProfile> :
<OtherProfile publicKey={params.id!}></OtherProfile>}
</>
);
}

View File

@@ -0,0 +1,83 @@
import { Breadcrumbs } from "@/app/components/Breadcrumbs/Breadcrumbs";
import { InternalScreen } from "@/app/components/InternalScreen/InternalScreen";
import { SettingsInput } from "@/app/components/SettingsInput/SettingsInput";
import { useAccount } from "@/app/providers/AccountProvider/useAccount";
import { useAccountProvider } from "@/app/providers/AccountProvider/useAccountProvider";
import { usePublicKey } from "@/app/providers/AccountProvider/usePublicKey";
import { Text } from "@mantine/core";
import { modals } from "@mantine/modals";
import { useNavigate } from "react-router-dom";
export function Safety() {
const publicKey = usePublicKey();
const [account, deleteAccount] = useAccount();
const {removeAccountFromLoginDice} = useAccountProvider();
const navigate = useNavigate();
const openDeleteModal = () => {
modals.openConfirmModal({
title: 'Account delete',
centered: true,
children: (
<Text size="sm">
You are attempting to delete your account. Are you sure you want to proceed? This will result in a complete loss of data.
</Text>
),
withCloseButton: false,
labels: { confirm: 'Continue', cancel: "No, sorry" },
confirmProps: { color: 'red' },
onConfirm: accountDelete
});
}
const accountDelete = () => {
removeAccountFromLoginDice();
deleteAccount();
navigate("/");
}
return (
<>
<Breadcrumbs text="Safety"></Breadcrumbs>
<InternalScreen>
<SettingsInput.Copy
mt={'sm'}
hit="Public Key"
value={publicKey}
placeholder="Public"></SettingsInput.Copy>
<Text fz={10} mt={3} c={'gray'} pl={'xs'} pr={'xs'}>
This is your public key. If you haven't set a @username yet, you can ask a friend to message you using your public key.
</Text>
<SettingsInput.Default
mt={'sm'}
disabled
hit="Private key"
value={account.privateKey}></SettingsInput.Default>
<Text fz={10} mt={3} c={'gray'} pl={'xs'} pr={'xs'}>
This is your private key. For security reasons, we provide it only in an encrypted form so you can simply admire it.
If anyone asks you for this key, please do not share it.
</Text>
<SettingsInput.Clickable mt={'sm'} c={'red'}
hit="Backup"
onClick={() => navigate("/main/backup")}
>
</SettingsInput.Clickable>
<Text fz={10} mt={3} c={'gray'} pl={'xs'} pr={'xs'}>
Please save your seed phrase, it is necessary for future access to your conversations.
Do not share this seed phrase with anyone, otherwise, the person you share it with will gain access to your conversations.
</Text>
<SettingsInput.Clickable mt={'sm'} c={'red'}
hit="Delete Account"
onClick={openDeleteModal}
rightChevronHide
>
</SettingsInput.Clickable>
<Text fz={10} mt={3} c={'gray'} pl={'xs'} pr={'xs'}>
This action cannot be undone, it will result in the complete deletion of account data from your device.
Please note, this will also delete your data on the server, such as your avatar, encrypted messages, and username.
</Text>
</InternalScreen>
</>
)
}

View File

@@ -0,0 +1,27 @@
.inner {
display: flex;
justify-content: center;
align-items: center;
padding-top: calc(var(--mantine-spacing-xl));
}
.content {
max-width: 480px;
}
.title {
color: light-dark(var(--mantine-color-black), var(--mantine-color-white));
font-family:
Greycliff CF,
var(--mantine-font-family);
font-size: 44px;
line-height: 1.2;
font-weight: 900;
}
.highlight {
position: relative;
background-color: var(--mantine-color-blue-light);
border-radius: var(--mantine-radius-sm);
padding: 4px 12px;
}

View File

@@ -0,0 +1,142 @@
import { Box, Flex, PasswordInput, Text, Transition } from "@mantine/core";
import { useEffect, useState } from "react";
import Lottie from "lottie-react";
import animationData from './lottie.json'
import { useMemory } from "@/app/providers/MemoryProvider/useMemory";
import { mnemonicToSeed } from "web-bip39";
import { useNavigate } from "react-router-dom";
import { modals } from "@mantine/modals";
import { Buffer } from 'buffer'
import { encodeWithPassword, generateHashFromPrivateKey, generateKeyPairFromSeed } from "@/app/crypto/crypto";
import { useAccountProvider } from "@/app/providers/AccountProvider/useAccountProvider";
import { Account } from "@/app/providers/AccountProvider/AccountProvider";
import { useRosettaColors } from "@/app/hooks/useRosettaColors";
import { AnimatedButton } from "@/app/components/AnimatedButton/AnimatedButton";
import { IconChevronRight } from "@tabler/icons-react";
import { SIZE_LOGIN_WIDTH_PX } from "@/app/constants";
export function SetPassword() {
const navigate = useNavigate();
const [password, setPassword] = useState("");
const [confirm, setConfirm] = useState("");
const [phrase, _] = useMemory("seed-phrase", "");
const [mounted, setMounted] = useState(false);
const colors = useRosettaColors();
const { createAccount, loginAccount, selectAccountToLoginDice } = useAccountProvider();
const openInsecurePasswordModal = () => {
modals.openConfirmModal({
title: 'Insecure password',
centered: true,
children: (
<Text size="sm">
Your password is insecure,
such passwords are easy to guess, come up with a new one, or, which is not recommended, leave this one
</Text>
),
withCloseButton: false,
labels: { confirm: 'Continue', cancel: "I'll come up with a new one" },
confirmProps: { color: 'red' },
onConfirm: doneSetup
});
}
useEffect(() => {
if (phrase.trim() == "") {
navigate("/");
}
setTimeout(() => setMounted(true), 100);
}, []);
const doneSetup = async () => {
let seed = await mnemonicToSeed(phrase);
let hex = Buffer.from(seed).toString('hex');
let keypair = await generateKeyPairFromSeed(hex);
const encrypted = await encodeWithPassword(password, keypair.privateKey);
const privateKeyHash = await generateHashFromPrivateKey(keypair.privateKey);
const account: Account = {
publicKey: keypair.publicKey,
privateKey: encrypted,
privatePlain: keypair.privateKey,
privateHash: privateKeyHash,
seedPhraseEncrypted: await encodeWithPassword(password, phrase)
}
createAccount(account);
loginAccount(account);
selectAccountToLoginDice(account);
navigate("/main");
}
const onDone = async () => {
if (!password.match(/[A-Z]+/) || !password.match(/[0-9]+/) || !password.match(/[$@#&!]+/)) {
openInsecurePasswordModal();
return;
}
doneSetup();
}
return (
<Flex h={520} w={385}>
<Flex h={'100%'} w={'100%'}>
<Flex h={'100%'} w={'100%'} pb={'sm'} align={'center'} direction={'column'} justify={'space-between'}>
<Box mt={'xl'}>
<Flex gap={'sm'} w={SIZE_LOGIN_WIDTH_PX} direction={'column'} align={'center'}>
<Lottie animationData={animationData} style={{
width: 100,
height: 100
}} loop={false}></Lottie>
<Flex w={'100%'} gap={'sm'} direction={'column'} mt={'sm'} pl={'sm'} pr={'sm'} align={'center'}>
<Text ta={'center'} fw={500} size={'sm'}>
Protect account
</Text>
<Text c={'dimmed'} ta={'center'} size={'xs'}>
Create a password to protect your account
</Text>
</Flex>
</Flex>
<Transition mounted={mounted} transition="slide-up" duration={400} timingFunction="ease">
{(styles) => (
<Flex w={SIZE_LOGIN_WIDTH_PX} mt={'xl'} pr={'sm'} pl={'sm'} direction={'column'} align={'center'} justify={'center'} gap={'lg'} style={styles}>
<PasswordInput styles={{
input: {
border: '0px solid ' + colors.borderColor,
backgroundColor: colors.mainColor
},
error: {
color: colors.error
}
}} w={'100%'} placeholder="Password" value={password} onChange={(e) => setPassword(e.target.value)} size="md"></PasswordInput>
<PasswordInput styles={{
input: {
border: '0px solid ' + colors.borderColor,
backgroundColor: colors.mainColor
},
error: {
color: colors.error
}
}} w={'100%'} placeholder="Confirm" value={confirm} onChange={(e) => setConfirm(e.target.value)} size="md"></PasswordInput>
<AnimatedButton disabled={password.length === 0 || confirm.length === 0 || password !== confirm} rightSection={<IconChevronRight size={16}></IconChevronRight>} animated={['#2DA5FF', '#87DBFF']} fullWidth onClick={onDone} size={'md'}>
Start
</AnimatedButton>
</Flex>
)}
</Transition>
</Box>
<Transition mounted={mounted} transition="slide-up" duration={400} timingFunction="ease">
{(styles) => (
<Flex pr={'sm'} pl={'sm'} w={'100%'} align={'center'} justify={'center'} gap={'lg'} style={styles}>
<Text c={'dimmed'} ta={'center'} w={'100%'} size={'xs'}>
Your password <Text c={'blue'} fw={500} span>never leaves</Text> your device and is never stored anywhere.
</Text>
</Flex>
)}
</Transition>
</Flex>
</Flex>
</Flex>)
}

File diff suppressed because one or more lines are too long

295
app/views/Theme/Theme.tsx Normal file
View File

@@ -0,0 +1,295 @@
import { Breadcrumbs } from "@/app/components/Breadcrumbs/Breadcrumbs";
import { InternalScreen } from "@/app/components/InternalScreen/InternalScreen";
import { SettingsInput } from "@/app/components/SettingsInput/SettingsInput";
import { useRosettaColors } from "@/app/hooks/useRosettaColors";
import { Box, darken, Divider, Flex, Image, lighten, Paper, Text, useComputedColorScheme, useMantineColorScheme, useMantineTheme } from "@mantine/core";
import lightThemeImage from './comments-light.svg'
import darkThemeImage from './comments-dark.svg'
import { useSetting } from "@/app/providers/SettingsProvider/useSetting";
import { SettingsPaper } from "@/app/components/SettingsPaper/SettingsPaper";
import { Message, MessageStyle, MessageSystem } from "@/app/components/Messages/Message";
import { DeliveredMessageState, DialogProvider } from "@/app/providers/DialogProvider/DialogProvider";
import { AttachmentType } from "@/app/providers/ProtocolProvider/protocol/packets/packet.message";
import { usePublicKey } from "@/app/providers/AccountProvider/usePublicKey";
import { WALLPAPERS } from "@/app/wallpapers/wallpapers";
import { useEffect, useRef } from "react";
export function Theme() {
const {setColorScheme} = useMantineColorScheme({
keepTransitions: true
});
const currentColorSchemeValue = useMantineColorScheme().colorScheme;
const colorScheme = useComputedColorScheme();
const colors = useRosettaColors();
const theme = useMantineTheme();
const publicKey = usePublicKey();
const scrollContainerRef = useRef<HTMLDivElement>(null);
const [showAlertInReplyMessages, setShowAlertInReplyMessages] = useSetting<boolean>
('showAlertInReplyMessages', true);
const [showTimeInReplyMessages, setShowTimeInReplyMessages] = useSetting<boolean>
('showTimeInReplyMessages', false);
const [bgInReplyMessages, setBgInReplyMessages] = useSetting<string>
('bgInReplyMessages', '');
const [messageStyle, setMessageStyle] = useSetting<MessageStyle>
('messageStyle', MessageStyle.ROWS);
const [wallpaper, setWallpaper] = useSetting<string>
('wallpaper', '');
const themeSwitcherColor = colorScheme == 'light' ? darken(colors.chevrons.active, 0.6) :
lighten(colors.chevrons.active, 0.6);
const colorSchemeSwitch = (scheme : any) => {
setColorScheme(scheme);
}
useEffect(() => {
const handleWheel = (event: WheelEvent) => {
event.preventDefault();
if (scrollContainerRef.current) {
scrollContainerRef.current.scrollLeft += event.deltaY;
}
};
const element = scrollContainerRef.current;
if (element) {
element.addEventListener('wheel', handleWheel, { passive: false });
}
return () => {
if (element) {
element.removeEventListener('wheel', handleWheel);
}
};
}, []);
return (<>
<Breadcrumbs text="Theme"></Breadcrumbs>
<InternalScreen>
<SettingsPaper>
<Box style={{
width: '100%',
pointerEvents: 'none',
backgroundImage: wallpaper != '' ? `url(${wallpaper})` : undefined,
backgroundSize: 'cover',
backgroundPosition: 'center',
padding: '10px',
borderTopLeftRadius: '8px',
borderTopRightRadius: '8px',
border: '1px solid ' + colors.borderColor
}}>
<DialogProvider dialog="demo">
<MessageSystem message="today" ></MessageSystem>
<Message
message="Hello! :emoji_1f600:"
timestamp={Math.floor(Date.now())}
delivered={DeliveredMessageState.DELIVERED}
chacha_key_plain=""
from_me={true}
readed={true}
from={""}
attachments={[]}
message_id="example-message-id"
is_last_message_in_stack={true}
/>
<Message
message="How are you? :emoji_1f481:"
timestamp={Math.floor(Date.now() + 60000)}
delivered={DeliveredMessageState.DELIVERED}
chacha_key_plain=""
from_me={false}
from="0x000000000000000000000000000000000000000001"
attachments={[{
id: "",
preview: "",
type: AttachmentType.MESSAGES,
blob: JSON.stringify([
{
publicKey: publicKey,
timestamp: Math.floor(Date.now()),
message: "Hello! :emoji_1f600:",
attachments: []
}
])
}]}
is_last_message_in_stack={true}
message_id="example-message-id"
/>
<Message
message="Good :emoji_1f639:!"
timestamp={Math.floor(Date.now() - 60000)}
delivered={DeliveredMessageState.DELIVERED}
chacha_key_plain=""
from_me={true}
from="0x000000000000000000000000000000000000000001"
attachments={[]}
message_id="example-message-id"
is_last_message_in_stack={true}
/>
</DialogProvider>
</Box>
<Divider w={'100%'}></Divider>
<Flex ref={scrollContainerRef} p={'xs'} style={{
overflowX: 'scroll',
scrollbarWidth: 'none',
}} direction={'row'} align={'center'}>
<Flex direction={'column'} onClick={() => colorSchemeSwitch('light')} align={'center'} justify={'center'}>
<Paper style={{
border: colorScheme == 'light' ? '2px solid var(--mantine-primary-color-6)' : undefined
}} bg={'white'} withBorder w={130} h={80}>
<Image src={lightThemeImage}></Image>
</Paper>
<Text mt={'xs'} size={'xs'} fw={colorScheme == 'light' ? 600 : undefined} c={themeSwitcherColor}>Light</Text>
</Flex>
<Flex direction={'column'} onClick={() => colorSchemeSwitch('dark')} align={'center'} justify={'center'}>
<Paper style={{
border: colorScheme == 'light' ? undefined : '2px solid var(--mantine-primary-color-6)'
}} ml={'sm'} bg={theme.colors.dark[7]} withBorder w={130} h={80}>
<Image src={darkThemeImage}></Image>
</Paper>
<Text mt={'xs'} size={'xs'} fw={colorScheme == 'light' ? undefined : 600} c={themeSwitcherColor}>Dark</Text>
</Flex>
<Divider orientation={'vertical'} ml={'sm'}></Divider>
<Flex direction={'column'} onClick={() => setWallpaper('')} align={'center'} justify={'center'}>
<Paper style={{
border: wallpaper == '' ? '2px solid var(--mantine-primary-color-6)' : undefined
}} ml={'sm'} bg={colorScheme == 'dark' ? 'white' : 'dark'} withBorder w={130} h={80}></Paper>
<Text mt={'xs'} size={'xs'} fw={colorScheme == 'light' ? undefined : 600} c={themeSwitcherColor}>No wallpaper</Text>
</Flex>
{WALLPAPERS.map((wp, index) => (
<Flex style={{
cursor: 'pointer'
}} key={index} direction={'column'} onClick={() => {
setWallpaper(wp.src);
setMessageStyle(MessageStyle.BUBBLES);
}} align={'center'} justify={'center'}>
<Paper style={{
border: wallpaper == wp.src ? '2px solid var(--mantine-primary-color-6)' : undefined
}} ml={'sm'} bg={theme.colors.dark[7]} withBorder w={130} h={80}>
<Image src={wp.src} w={'100%'} h={'100%'} radius={'sm'}></Image>
</Paper>
<Text mt={'xs'} size={'xs'} fw={colorScheme == 'light' ? undefined : 600} c={themeSwitcherColor}>{wp.name}</Text>
</Flex>
))}
</Flex>
</SettingsPaper>
{/* <Paper withBorder styles={{
root: {
borderTop: '1px solid ' + colors.borderColor,
borderBottom: '1px solid ' + colors.borderColor,
borderLeft: '1px solid ' + colors.borderColor,
borderRight: '1px solid ' + colors.borderColor,
cursor: 'pointer'
}
}}
>
<Flex direction={'row'} p={'sm'} justify={'space-between'}>
<Text size={'sm'} fw={400}>Theme</Text>
<div style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-end'
}}>
<Flex direction={'column'} onClick={() => colorSchemeSwitch('light')} align={'center'} justify={'center'}>
<Paper style={{
border: colorScheme == 'light' ? '2px solid var(--mantine-primary-color-6)' : undefined
}} bg={'white'} withBorder w={130} h={80}>
<Image src={lightThemeImage}></Image>
</Paper>
<Text mt={'xs'} size={'xs'} fw={colorScheme == 'light' ? 600 : undefined} c={themeSwitcherColor}>Light</Text>
</Flex>
<Flex direction={'column'} onClick={() => colorSchemeSwitch('dark')} align={'center'} justify={'center'}>
<Paper style={{
border: colorScheme == 'light' ? undefined : '2px solid var(--mantine-primary-color-6)'
}} ml={'sm'} bg={theme.colors.dark[7]} withBorder w={130} h={80}>
<Image src={darkThemeImage}></Image>
</Paper>
<Text mt={'xs'} size={'xs'} fw={colorScheme == 'light' ? undefined : 600} c={themeSwitcherColor}>Dark</Text>
</Flex>
</div>
</Flex>
</Paper> */}
{/* <Paper mt={'md'} withBorder styles={{
root: {
borderTop: '1px solid ' + colors.borderColor,
borderBottom: '1px solid ' + colors.borderColor,
borderLeft: '1px solid ' + colors.borderColor,
borderRight: '1px solid ' + colors.borderColor,
}
}}
>
<Message
message="This is an example of reply messages attachment. Reply messages may be forged by sender."
timestamp={Math.floor(Date.now())}
delivered={DeliveredMessageState.DELIVERED}
chacha_key_plain=""
from_me={false}
from="0x000000000000000000000000000000000000000001"
attachments={[{
type: AttachmentType.MESSAGES,
blob: JSON.stringify(exampleMessages),
id: 'attachment-example-id',
preview: ""
}]}
message_id="example-message-id"
/>
</Paper> */}
<SettingsInput.Switch
hit="Dark theme"
defaultValue={currentColorSchemeValue == 'dark'}
mt={'sm'}
onChange={(v) => colorSchemeSwitch(v ? 'dark' : 'light')}
>
</SettingsInput.Switch>
<SettingsInput.Switch
hit="Messages as bubbles"
defaultValue={messageStyle == MessageStyle.BUBBLES || wallpaper != ''}
mt={'sm'}
onChange={(v) => {
if(!v) {
setWallpaper('');
}
setMessageStyle(v ? MessageStyle.BUBBLES : MessageStyle.ROWS)
}}
>
</SettingsInput.Switch>
<Text fz={10} mt={3} c={'gray'} pl={'xs'} pr={'xs'}>
Enable this option to display messages in bubble style, similar to many modern messaging apps.
</Text>
<SettingsInput.Switch
hit="Show alerts in reply messages"
defaultValue={showAlertInReplyMessages}
mt={'sm'}
onChange={(v) => setShowAlertInReplyMessages(v)}
>
</SettingsInput.Switch>
<Text fz={10} mt={3} c={'gray'} pl={'xs'} pr={'xs'}>
Reply messages may be forged by sender, if its option enabled you will see alert in such messages.
</Text>
<SettingsInput.Switch
hit="Show time in reply messages"
defaultValue={showTimeInReplyMessages}
mt={'sm'}
onChange={(v) => setShowTimeInReplyMessages(v)}
>
</SettingsInput.Switch>
<Text fz={10} mt={3} c={'gray'} pl={'xs'} pr={'xs'}>
If this option is enabled, the time will be displayed for each reply message.
</Text>
<SettingsInput.Select
hit={'Background color'}
mt={'sm'}
width={200}
defaultValue={bgInReplyMessages == '' ? 'none' : bgInReplyMessages}
onChange={(v) => {
setBgInReplyMessages(v == 'none' ? '' : v!);
}}
variants={['none', 'red', 'pink', 'grape', 'violet', 'indigo', 'blue', 'cyan', 'teal', 'green', 'lime', 'yellow', 'orange', 'brown', 'gray']}
></SettingsInput.Select>
<Text fz={10} mt={3} c={'gray'} pl={'xs'} pr={'xs'}>
This setting allows you to choose a background color for reply messages to make them stand out more clearly.
</Text>
</InternalScreen>
</>)
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.5 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.5 KiB

View File

@@ -0,0 +1,38 @@
import { Breadcrumbs } from "@/app/components/Breadcrumbs/Breadcrumbs";
import { InternalScreen } from "@/app/components/InternalScreen/InternalScreen";
import { RosettaPower } from "@/app/components/RosettaPower/RosettaPower";
import { SettingsAlert } from "@/app/components/SettingsAlert/SettingsAlert";
import { SettingsInput } from "@/app/components/SettingsInput/SettingsInput";
import { UpdateAlert } from "@/app/components/UpdateAlert/UpdateAlert";
import { CORE_VERSION } from "@/app/constants";
import { useUpdater } from "@/app/hooks/useUpdater";
import { APP_VERSION } from "@/app/version";
import { Box, Text } from "@mantine/core";
export function Update() {
const {appUpdateUrl, kernelUpdateUrl, kernelOutdatedForNextAppUpdates} = useUpdater();
return (
<>
<Breadcrumbs text="Updates"></Breadcrumbs>
<InternalScreen>
{(kernelUpdateUrl != "" || appUpdateUrl != "" || kernelOutdatedForNextAppUpdates) && (
<SettingsAlert type="error" text="We recommend always using the latest version of the application. You can also update the app using the button in the left menu below the list of dialogs."></SettingsAlert>
)}
<Box mt={'sm'}>
<UpdateAlert radius={'sm'}></UpdateAlert>
</Box>
<SettingsInput.Copy mt={'sm'} hit="Kernel" value={CORE_VERSION}></SettingsInput.Copy>
<Text fz={10} mt={3} c={'gray'} pl={'xs'} pr={'xs'}>
If the kernel version is outdated, you need to reinstall the application so that this kernel continues to receive current updates.
</Text>
<SettingsInput.Copy mt={'sm'} hit="Application" value={APP_VERSION}></SettingsInput.Copy>
<Text fz={10} mt={3} c={'gray'} pl={'xs'} pr={'xs'}>
App version. We recommend always keeping it up to date to improve visual effects and have the latest features.
</Text>
<RosettaPower mt={'lg'}></RosettaPower>
</InternalScreen>
</>
)
}