'init'
This commit is contained in:
BIN
app/views/.DS_Store
vendored
Normal file
BIN
app/views/.DS_Store
vendored
Normal file
Binary file not shown.
53
app/views/Backup/Backup.tsx
Normal file
53
app/views/Backup/Backup.tsx
Normal 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
42
app/views/Chat/Chat.tsx
Normal 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>
|
||||
</>);
|
||||
}
|
||||
59
app/views/ConfirmSeed/ConfirmSeed.tsx
Normal file
59
app/views/ConfirmSeed/ConfirmSeed.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
59
app/views/CreateGroup/CreateGroup.tsx
Normal file
59
app/views/CreateGroup/CreateGroup.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
1
app/views/CreateGroup/users.json
Normal file
1
app/views/CreateGroup/users.json
Normal file
File diff suppressed because one or more lines are too long
95
app/views/CreateSeed/CreateSeed.tsx
Normal file
95
app/views/CreateSeed/CreateSeed.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
59
app/views/DeviceConfirm/DeviceConfirm.tsx
Normal file
59
app/views/DeviceConfirm/DeviceConfirm.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
1
app/views/DeviceConfirm/inbox.json
Normal file
1
app/views/DeviceConfirm/inbox.json
Normal file
File diff suppressed because one or more lines are too long
11
app/views/DialogPreview/DialogPreview.tsx
Normal file
11
app/views/DialogPreview/DialogPreview.tsx
Normal 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>
|
||||
</>);
|
||||
}
|
||||
12
app/views/Dialogs/Dialogs.tsx
Normal file
12
app/views/Dialogs/Dialogs.tsx
Normal 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>}
|
||||
</>
|
||||
)
|
||||
}
|
||||
27
app/views/ExistsSeed/ExistsSeed.module.css
Normal file
27
app/views/ExistsSeed/ExistsSeed.module.css
Normal 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;
|
||||
}
|
||||
82
app/views/ExistsSeed/ExistsSeed.tsx
Normal file
82
app/views/ExistsSeed/ExistsSeed.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
74
app/views/GroupEncryption/GroupEncryption.tsx
Normal file
74
app/views/GroupEncryption/GroupEncryption.tsx
Normal 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>
|
||||
</>)
|
||||
}
|
||||
168
app/views/GroupInfo/GroupInfo.tsx
Normal file
168
app/views/GroupInfo/GroupInfo.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
168
app/views/Introduction/Introduction.module.css
Normal file
168
app/views/Introduction/Introduction.module.css
Normal 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));
|
||||
}
|
||||
160
app/views/Introduction/Introduction.tsx
Normal file
160
app/views/Introduction/Introduction.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
28
app/views/Lockscreen/Lockscreen.module.css
Normal file
28
app/views/Lockscreen/Lockscreen.module.css
Normal 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;
|
||||
}
|
||||
190
app/views/Lockscreen/Lockscreen.tsx
Normal file
190
app/views/Lockscreen/Lockscreen.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
5
app/views/Main/Main.module.css
Normal file
5
app/views/Main/Main.module.css
Normal file
@@ -0,0 +1,5 @@
|
||||
.dialogs_wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
179
app/views/Main/Main.tsx
Normal file
179
app/views/Main/Main.tsx
Normal 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>);
|
||||
}
|
||||
143
app/views/Profile/MyProfile.tsx
Normal file
143
app/views/Profile/MyProfile.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
68
app/views/Profile/OtherProfile.tsx
Normal file
68
app/views/Profile/OtherProfile.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
9
app/views/Profile/Profile.module.css
Normal file
9
app/views/Profile/Profile.module.css
Normal 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);
|
||||
}
|
||||
16
app/views/Profile/Profile.tsx
Normal file
16
app/views/Profile/Profile.tsx
Normal 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>}
|
||||
</>
|
||||
);
|
||||
}
|
||||
83
app/views/Safety/Safety.tsx
Normal file
83
app/views/Safety/Safety.tsx
Normal 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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
27
app/views/SetPassword/SetPassword.module.css
Normal file
27
app/views/SetPassword/SetPassword.module.css
Normal 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;
|
||||
}
|
||||
142
app/views/SetPassword/SetPassword.tsx
Normal file
142
app/views/SetPassword/SetPassword.tsx
Normal 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>)
|
||||
}
|
||||
1
app/views/SetPassword/lottie.json
Normal file
1
app/views/SetPassword/lottie.json
Normal file
File diff suppressed because one or more lines are too long
295
app/views/Theme/Theme.tsx
Normal file
295
app/views/Theme/Theme.tsx
Normal 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>
|
||||
</>)
|
||||
}
|
||||
1
app/views/Theme/comments-dark.svg
Normal file
1
app/views/Theme/comments-dark.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 5.5 KiB |
1
app/views/Theme/comments-light.svg
Normal file
1
app/views/Theme/comments-light.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 5.5 KiB |
38
app/views/Update/Update.tsx
Normal file
38
app/views/Update/Update.tsx
Normal 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>
|
||||
</>
|
||||
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user