├── .nyrc.json ├── .nyc_output └── out.json ├── public ├── _redirects ├── xxdk-wasm ├── favicon.ico ├── preview.jpeg ├── haven-icon.png ├── sounds │ ├── augh.mp3 │ ├── bring.mp3 │ ├── parry.mp3 │ ├── classic-icq.wav │ └── notification.mp3 ├── fonts │ └── RobotoFlex-Regular.ttf └── favicon.svg ├── @types ├── emoji-mart__react.d.ts ├── redux-localstorage.d.ts ├── react-multiline-clamp.d.ts └── react-app-env.d.ts ├── src ├── components │ ├── views │ │ ├── Login │ │ │ └── index.ts │ │ ├── Register │ │ │ ├── index.ts │ │ │ └── Register.tsx │ │ ├── JoinChannel │ │ │ └── index.ts │ │ ├── LoadingView.tsx │ │ └── SettingsViews │ │ │ ├── index.tsx │ │ │ ├── NotificationsView.tsx │ │ │ ├── LogOut.tsx │ │ │ └── ExportCodename.tsx │ ├── common │ │ ├── Collapse │ │ │ └── index.ts │ │ ├── Loading │ │ │ ├── index.ts │ │ │ └── Loading.tsx │ │ ├── Spinner │ │ │ └── index.ts │ │ ├── ChannelChat │ │ │ ├── index.ts │ │ │ ├── SendButton │ │ │ │ └── index.tsx │ │ │ ├── PinnedMessage │ │ │ │ └── index.tsx │ │ │ └── MessagesContainer │ │ │ │ └── index.tsx │ │ ├── LeftSideBar │ │ │ ├── index.ts │ │ │ └── LeftSideBar.tsx │ │ ├── ProgressBar │ │ │ ├── index.ts │ │ │ └── ProgressBar.tsx │ │ ├── WebAssemblyRunner │ │ │ ├── xxdk-wasm.d.ts │ │ │ └── index.ts │ │ ├── ImportCodeNameLoading │ │ │ ├── index.ts │ │ │ └── ImportCodeNameLoading.tsx │ │ ├── RightSideBar │ │ │ ├── RightSideTitle.tsx │ │ │ └── index.tsx │ │ ├── CloseButton.tsx │ │ ├── FormError.tsx │ │ ├── WarningComponent │ │ │ └── index.tsx │ │ ├── index.ts │ │ ├── LeftHeader │ │ │ ├── index.tsx │ │ │ ├── User.tsx │ │ │ └── SidebarControls.tsx │ │ ├── Input │ │ │ └── index.tsx │ │ ├── Badge.tsx │ │ ├── ErrorBoundary.tsx │ │ ├── SearchInput.tsx │ │ ├── CheckboxToggle │ │ │ └── index.tsx │ │ ├── MainHeader │ │ │ └── index.tsx │ │ ├── Button │ │ │ └── index.tsx │ │ ├── NotificationBanner │ │ │ └── index.tsx │ │ ├── EnumerateList │ │ │ └── index.tsx │ │ ├── ChannelBadges.tsx │ │ └── Alert │ │ │ └── index.tsx │ ├── modals │ │ ├── LogoutView │ │ │ └── index.ts │ │ ├── JoinChannelView │ │ │ └── index.ts │ │ ├── NickNameSetView │ │ │ └── index.ts │ │ ├── ShareChannelView │ │ │ └── index.ts │ │ ├── ChannelSettingsView │ │ │ └── index.ts │ │ ├── CreateChannelView │ │ │ └── index.ts │ │ ├── ExportCodenameView │ │ │ └── index.ts │ │ ├── NetworkNotReadyView │ │ │ ├── index.ts │ │ │ ├── NetworkNotReadyView.tsx │ │ │ └── index.tsx │ │ ├── JoinChannelSuccessView │ │ │ ├── index.ts │ │ │ └── JoinChannelSuccessView.tsx │ │ ├── ImportAccountView │ │ │ ├── ImportAccountForm │ │ │ │ └── index.ts │ │ │ ├── types.ts │ │ │ └── index.tsx │ │ ├── LeaveChannelConfirmationView │ │ │ ├── index.ts │ │ │ └── LeaveChannelConfirmationView.tsx │ │ ├── ModalTitle.tsx │ │ ├── UserWasMuted │ │ │ └── index.tsx │ │ ├── index.ts │ │ ├── ViewPinnedMessages │ │ │ └── index.tsx │ │ ├── DeleteMessage │ │ │ └── index.tsx │ │ ├── PinMessageModal │ │ │ └── index.tsx │ │ ├── MuteUser │ │ │ └── index.tsx │ │ ├── Modal │ │ │ └── index.tsx │ │ ├── AccountSync │ │ │ └── index.tsx │ │ ├── Modal.tsx │ │ └── AppModals.tsx │ └── icons │ │ ├── NetworkStatusIcon │ │ ├── index.ts │ │ └── NetworkStatusIcon.tsx │ │ ├── MissedMessagesIcon │ │ ├── index.ts │ │ └── MissedMessagesIcon.tsx │ │ ├── Slash.tsx │ │ ├── CommentSlash.tsx │ │ ├── RightFromBracket.tsx │ │ ├── ArrowDown.tsx │ │ ├── ArrowUp.tsx │ │ ├── ConnectingLine.tsx │ │ ├── Checkmark.tsx │ │ ├── X.tsx │ │ ├── DoubleLeftArrows.tsx │ │ ├── DoubleRightArrows.tsx │ │ ├── Dev.tsx │ │ ├── Warning.tsx │ │ ├── Plus.tsx │ │ ├── Ellipsis.tsx │ │ ├── Close.tsx │ │ ├── Send.tsx │ │ ├── Italics.tsx │ │ ├── NormalHash.tsx │ │ ├── LockOpen.tsx │ │ ├── Add.tsx │ │ ├── Elixxir.tsx │ │ ├── Block.tsx │ │ ├── Keys.tsx │ │ ├── Unpin.tsx │ │ ├── Envelope.tsx │ │ ├── Spaces.tsx │ │ ├── Blockquote.tsx │ │ ├── Code.tsx │ │ ├── RoadMap.tsx │ │ ├── Delete.tsx │ │ ├── Error.tsx │ │ ├── index.ts │ │ ├── Chat.tsx │ │ ├── OpenSource.tsx │ │ ├── Notice.tsx │ │ ├── Copy.tsx │ │ ├── Reply.tsx │ │ ├── Download.tsx │ │ ├── Export.tsx │ │ ├── Upload.tsx │ │ ├── Bold.tsx │ │ ├── Profile.tsx │ │ ├── BulletList.tsx │ │ ├── Join.tsx │ │ ├── RTF.tsx │ │ ├── Logout.tsx │ │ ├── Share.tsx │ │ ├── CodeBlock.tsx │ │ ├── Edit.tsx │ │ ├── EmojisPicker.tsx │ │ ├── OrderedList.tsx │ │ └── Leave.tsx ├── layouts │ ├── DefaultLayout │ │ ├── index.ts │ │ └── ConnectingDimmer.tsx │ └── index.ts ├── events │ ├── index.ts │ └── dm.ts ├── hooks │ ├── useRemoteStore.ts │ ├── usePrevious.tsx │ ├── useIsMountedRef.ts │ ├── useChannelsStorageTag.ts │ ├── useInput.ts │ ├── useNetworkTrackPeriod.ts │ ├── useCopyToClipboard.ts │ ├── useAccountSync.ts │ ├── useAsync.ts │ ├── useChannelFavorites.ts │ ├── useToggle.ts │ ├── useIsDev.ts │ ├── useAudioContext.tsx │ ├── useLocalStorage.ts │ └── useStep.ts ├── assets │ └── images │ │ ├── haven-icon.png │ │ ├── haven-logo.png │ │ ├── logo.svg │ │ ├── notice.svg │ │ ├── profile.svg │ │ └── join.svg ├── utils │ ├── extend-dayjs.ts │ ├── date.ts │ └── compression.ts ├── i18n │ ├── i18next.d.ts │ ├── locales.ts │ ├── cache.ts │ ├── index.ts │ └── backend.ts ├── quill │ ├── constants.js │ └── EmojiBlot.ts ├── store │ ├── identity │ │ ├── selectors.ts │ │ ├── types.ts │ │ └── index.ts │ ├── hooks.ts │ ├── types.ts │ ├── index.ts │ ├── app │ │ ├── types.ts │ │ └── selectors.ts │ ├── channels │ │ └── types.ts │ ├── dms │ │ └── types.ts │ ├── messages │ │ └── types.ts │ ├── selectors.ts │ └── utils.ts ├── env.d.ts ├── types │ ├── ui.ts │ ├── json.ts │ ├── db.ts │ └── emitter.ts ├── router.tsx ├── main.tsx ├── contexts │ └── sound-context.tsx ├── declarations │ └── app │ │ ├── index.js │ │ └── index.d.ts ├── constants.ts ├── routes │ └── Home.tsx └── App.tsx ├── canister_ids.json ├── postcss.config.js ├── cypress └── support │ ├── commands.ts │ ├── cypress.d.ts │ └── e2e.ts ├── postcss.config.cjs ├── .prettierrc ├── dfx.json ├── index.html ├── cypress.config.ts ├── .env.example ├── .vscode └── launch.json ├── .gitlab-ci.yml ├── local.Dockerfile ├── .gitignore ├── tsconfig.json ├── netlify.toml ├── Dockerfile ├── README.md ├── LICENSE ├── eslint.config.js └── tailwind.config.js /.nyrc.json: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.nyc_output/out.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /public/_redirects: -------------------------------------------------------------------------------- 1 | /* /index.html 200 -------------------------------------------------------------------------------- /public/xxdk-wasm: -------------------------------------------------------------------------------- 1 | ../node_modules/xxdk-wasm -------------------------------------------------------------------------------- /@types/emoji-mart__react.d.ts: -------------------------------------------------------------------------------- 1 | declare module '@emoji-mart/react'; 2 | -------------------------------------------------------------------------------- /@types/redux-localstorage.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'redux-localstorage'; 2 | -------------------------------------------------------------------------------- /@types/react-multiline-clamp.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'react-multiline-clamp'; 2 | -------------------------------------------------------------------------------- /src/components/views/Login/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './Login'; 2 | -------------------------------------------------------------------------------- /src/components/common/Collapse/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './Collapse'; 2 | -------------------------------------------------------------------------------- /src/components/common/Loading/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './Loading'; 2 | -------------------------------------------------------------------------------- /src/components/common/Spinner/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './Spinner'; 2 | -------------------------------------------------------------------------------- /src/components/views/Register/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './Register'; 2 | -------------------------------------------------------------------------------- /src/layouts/DefaultLayout/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './DefaultLayout'; 2 | -------------------------------------------------------------------------------- /src/components/common/ChannelChat/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './ChannelChat'; 2 | -------------------------------------------------------------------------------- /src/components/common/LeftSideBar/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './LeftSideBar'; 2 | -------------------------------------------------------------------------------- /src/components/common/ProgressBar/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './ProgressBar'; 2 | -------------------------------------------------------------------------------- /src/components/common/WebAssemblyRunner/xxdk-wasm.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'xxdk-wasm'; 2 | -------------------------------------------------------------------------------- /src/components/modals/LogoutView/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './LogoutView'; 2 | -------------------------------------------------------------------------------- /src/components/views/JoinChannel/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './JoinChannel'; 2 | -------------------------------------------------------------------------------- /src/layouts/index.ts: -------------------------------------------------------------------------------- 1 | export { default as DefaultLayout } from './DefaultLayout'; 2 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xxfoundation/haven/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/preview.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xxfoundation/haven/HEAD/public/preview.jpeg -------------------------------------------------------------------------------- /@types/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | declare module '*.mp3'; 3 | -------------------------------------------------------------------------------- /canister_ids.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": { 3 | "ic": "lbwo7-naaaa-aaaao-a3tha-cai" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /public/haven-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xxfoundation/haven/HEAD/public/haven-icon.png -------------------------------------------------------------------------------- /public/sounds/augh.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xxfoundation/haven/HEAD/public/sounds/augh.mp3 -------------------------------------------------------------------------------- /src/components/icons/NetworkStatusIcon/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './NetworkStatusIcon'; 2 | -------------------------------------------------------------------------------- /src/components/modals/JoinChannelView/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './JoinChannelView'; 2 | -------------------------------------------------------------------------------- /src/components/modals/NickNameSetView/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './NickNameSetView'; 2 | -------------------------------------------------------------------------------- /src/components/modals/ShareChannelView/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './ShareChannelView'; 2 | -------------------------------------------------------------------------------- /public/sounds/bring.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xxfoundation/haven/HEAD/public/sounds/bring.mp3 -------------------------------------------------------------------------------- /public/sounds/parry.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xxfoundation/haven/HEAD/public/sounds/parry.mp3 -------------------------------------------------------------------------------- /src/components/common/WebAssemblyRunner/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './WebAssemblyRunner'; 2 | -------------------------------------------------------------------------------- /src/components/icons/MissedMessagesIcon/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './MissedMessagesIcon'; 2 | -------------------------------------------------------------------------------- /src/components/modals/ChannelSettingsView/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './ChannelSettingsView'; 2 | -------------------------------------------------------------------------------- /src/components/modals/CreateChannelView/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './CreateChannelView'; 2 | -------------------------------------------------------------------------------- /src/components/modals/ExportCodenameView/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './ExportCodenameView'; 2 | -------------------------------------------------------------------------------- /src/components/modals/NetworkNotReadyView/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './NetworkNotReadyView'; 2 | -------------------------------------------------------------------------------- /src/components/common/ImportCodeNameLoading/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './ImportCodeNameLoading'; 2 | -------------------------------------------------------------------------------- /src/events/index.ts: -------------------------------------------------------------------------------- 1 | export * from './app'; 2 | export * from './channels'; 3 | export * from './dm'; 4 | -------------------------------------------------------------------------------- /src/hooks/useRemoteStore.ts: -------------------------------------------------------------------------------- 1 | export { useRemoteStore as default } from 'src/contexts/remote-kv-context'; 2 | -------------------------------------------------------------------------------- /public/sounds/classic-icq.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xxfoundation/haven/HEAD/public/sounds/classic-icq.wav -------------------------------------------------------------------------------- /src/components/modals/JoinChannelSuccessView/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './JoinChannelSuccessView'; 2 | -------------------------------------------------------------------------------- /public/sounds/notification.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xxfoundation/haven/HEAD/public/sounds/notification.mp3 -------------------------------------------------------------------------------- /src/assets/images/haven-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xxfoundation/haven/HEAD/src/assets/images/haven-icon.png -------------------------------------------------------------------------------- /src/assets/images/haven-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xxfoundation/haven/HEAD/src/assets/images/haven-logo.png -------------------------------------------------------------------------------- /src/components/modals/ImportAccountView/ImportAccountForm/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './ImportAccountForm'; 2 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/components/modals/LeaveChannelConfirmationView/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './LeaveChannelConfirmationView'; 2 | -------------------------------------------------------------------------------- /public/fonts/RobotoFlex-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xxfoundation/haven/HEAD/public/fonts/RobotoFlex-Regular.ttf -------------------------------------------------------------------------------- /cypress/support/commands.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | import '@testing-library/cypress/add-commands'; 3 | -------------------------------------------------------------------------------- /src/utils/extend-dayjs.ts: -------------------------------------------------------------------------------- 1 | import isToday from 'dayjs/plugin/isToday'; 2 | import dayjs from 'dayjs'; 3 | 4 | dayjs.extend(isToday); 5 | -------------------------------------------------------------------------------- /src/components/modals/ImportAccountView/types.ts: -------------------------------------------------------------------------------- 1 | export type IdentityVariables = { 2 | identity: string; 3 | password: string; 4 | }; 5 | -------------------------------------------------------------------------------- /postcss.config.cjs: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | module.exports = { 3 | plugins: { 4 | tailwindcss: {}, 5 | autoprefixer: {}, 6 | } 7 | } -------------------------------------------------------------------------------- /cypress/support/cypress.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace Cypress { 2 | interface ResolvedConfigOptions { 3 | hideXHRInCommandLog?: boolean; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/i18n/i18next.d.ts: -------------------------------------------------------------------------------- 1 | import 'i18next'; 2 | 3 | declare module 'i18next' { 4 | interface CustomTypeOptions { 5 | returnNull: false; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/quill/constants.js: -------------------------------------------------------------------------------- 1 | const Keys = { 2 | TAB: 9, 3 | ENTER: 13, 4 | ESCAPE: 27, 5 | UP: 38, 6 | DOWN: 40 7 | }; 8 | 9 | export default Keys; 10 | -------------------------------------------------------------------------------- /src/store/identity/selectors.ts: -------------------------------------------------------------------------------- 1 | import type { RootState } from 'src/store/types'; 2 | 3 | export const identity = (state: RootState) => state.identity.identity; 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "trailingComma": "none", 4 | "tabWidth": 2, 5 | "semi": true, 6 | "singleQuote": true, 7 | "jsxSingleQuote": true 8 | } 9 | -------------------------------------------------------------------------------- /src/components/icons/Slash.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | import { X, LucideProps } from 'lucide-react'; 3 | 4 | const Slash: FC = (props) => ; 5 | 6 | export default Slash; 7 | -------------------------------------------------------------------------------- /src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | interface ImportMetaEnv { 4 | readonly APP_VERSION: string; 5 | } 6 | 7 | interface ImportMeta { 8 | readonly env: ImportMetaEnv; 9 | } 10 | -------------------------------------------------------------------------------- /src/components/icons/CommentSlash.tsx: -------------------------------------------------------------------------------- 1 | import { MessageSquareOff } from 'lucide-react'; 2 | import { FC } from 'react'; 3 | 4 | const CommentSlash: FC = (props) => ; 5 | 6 | export default CommentSlash; 7 | -------------------------------------------------------------------------------- /src/i18n/locales.ts: -------------------------------------------------------------------------------- 1 | type Locale = { 2 | label: string; 3 | code: string; 4 | }; 5 | 6 | const locales: Locale[] = [ 7 | { 8 | label: 'English', 9 | code: 'en-US' 10 | } 11 | ]; 12 | 13 | export default locales; 14 | -------------------------------------------------------------------------------- /src/i18n/cache.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2017-2022 @polkadot/react-components authors & contributors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | const languageCache: Record> = {}; 5 | 6 | export default languageCache; 7 | -------------------------------------------------------------------------------- /src/components/icons/RightFromBracket.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | import { LogOut, LucideProps } from 'lucide-react'; 3 | 4 | const RightFromBracket: FC = (props) => ; 5 | 6 | export default RightFromBracket; 7 | -------------------------------------------------------------------------------- /dfx.json: -------------------------------------------------------------------------------- 1 | { 2 | "canisters": { 3 | "app": { 4 | "frontend": { 5 | "entrypoint": "dist/index.html" 6 | }, 7 | "source": ["dist"], 8 | "type": "assets" 9 | } 10 | }, 11 | "output_env_file": ".env" 12 | } 13 | -------------------------------------------------------------------------------- /src/store/hooks.ts: -------------------------------------------------------------------------------- 1 | import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'; 2 | import type { RootState, AppDispatch } from './index'; 3 | 4 | export const useAppDispatch = () => useDispatch(); 5 | export const useAppSelector: TypedUseSelectorHook = useSelector; 6 | -------------------------------------------------------------------------------- /src/components/modals/ModalTitle.tsx: -------------------------------------------------------------------------------- 1 | import { FC, HTMLAttributes } from 'react'; 2 | import cn from 'classnames'; 3 | 4 | const ModalTitle: FC> = (props) => ( 5 |

{props.children}

6 | ); 7 | 8 | export default ModalTitle; 9 | -------------------------------------------------------------------------------- /src/hooks/usePrevious.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react'; 2 | 3 | const usePrevious = (value: T) => { 4 | const ref = useRef(); 5 | 6 | useEffect(() => { 7 | ref.current = value; 8 | }, [value]); 9 | 10 | return ref.current; 11 | }; 12 | 13 | export default usePrevious; 14 | -------------------------------------------------------------------------------- /src/components/common/RightSideBar/RightSideTitle.tsx: -------------------------------------------------------------------------------- 1 | import type { WithChildren } from 'src/types'; 2 | import type { FC } from 'react'; 3 | 4 | const RightSideTitle: FC = ({ children }) => ( 5 |

{children}

6 | ); 7 | 8 | export default RightSideTitle; 9 | -------------------------------------------------------------------------------- /src/types/ui.ts: -------------------------------------------------------------------------------- 1 | export type LeftSidebarView = 'spaces' | 'dms' | 'settings'; 2 | export type RightSidebarView = 3 | | 'space-details' 4 | | 'user-details' 5 | | 'pinned-messages' 6 | | 'muted-users' 7 | | 'contributors' 8 | | 'channel-notifications'; 9 | export type SettingsView = 'notifications' | 'export-codename' | 'logout' | 'dev'; 10 | -------------------------------------------------------------------------------- /src/store/identity/types.ts: -------------------------------------------------------------------------------- 1 | export type Identity = { 2 | pubkey: string; 3 | codename: string; 4 | color: string; 5 | extension: string; 6 | codeset: number; 7 | }; 8 | 9 | export type IdentityState = { identity?: Identity }; 10 | 11 | declare module 'src/store/types' { 12 | interface RootState { 13 | identity: IdentityState; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/store/types.ts: -------------------------------------------------------------------------------- 1 | import { MessageStatus } from 'src/types'; 2 | 3 | export interface RootState { 4 | // empty on purpose so that the slices extend the interface 5 | } 6 | 7 | // { [messageId]: { [emoji]: codename[] } } 8 | export type ReactionInfo = { pubkey: string; codeset: number; id: string; status?: MessageStatus }; 9 | export type EmojiReactions = Record>; 10 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Haven Web 8 | 9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /src/components/icons/ArrowDown.tsx: -------------------------------------------------------------------------------- 1 | import { SVGProps } from 'react'; 2 | 3 | const ArrowDown = (props: SVGProps) => ( 4 | 12 | 13 | 14 | ); 15 | 16 | export default ArrowDown; 17 | -------------------------------------------------------------------------------- /cypress.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'cypress'; 2 | import codeCoverage from '@cypress/code-coverage/task'; 3 | 4 | export default defineConfig({ 5 | env: { 6 | codeCoverage: { 7 | url: '/api/__coverage__' 8 | } 9 | }, 10 | e2e: { 11 | hideXHRInCommandLog: true, 12 | baseUrl: 'http://127.0.0.1:3000/', 13 | setupNodeEvents(on, config) { 14 | codeCoverage(on, config); 15 | return config; 16 | } 17 | } 18 | }); 19 | -------------------------------------------------------------------------------- /src/components/views/LoadingView.tsx: -------------------------------------------------------------------------------- 1 | import type { FC } from 'react'; 2 | import { Spinner } from 'src/components/common'; 3 | 4 | type Props = { 5 | message?: string; 6 | }; 7 | 8 | const Loading: FC = ({ message }) => ( 9 |
10 | 11 | {message &&

{message}

} 12 |
13 | ); 14 | 15 | export default Loading; 16 | -------------------------------------------------------------------------------- /src/components/icons/ArrowUp.tsx: -------------------------------------------------------------------------------- 1 | import { SVGProps } from 'react'; 2 | 3 | const ArrowDown = (props: SVGProps) => ( 4 | 13 | 14 | 15 | ); 16 | 17 | export default ArrowDown; 18 | -------------------------------------------------------------------------------- /src/hooks/useIsMountedRef.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react'; 2 | 3 | export type MountedRef = React.MutableRefObject; 4 | 5 | function useIsMountedRef(): MountedRef { 6 | const isMounted = useRef(false); 7 | 8 | useEffect((): (() => void) => { 9 | isMounted.current = true; 10 | 11 | return (): void => { 12 | isMounted.current = false; 13 | }; 14 | }, []); 15 | 16 | return isMounted; 17 | } 18 | 19 | export default useIsMountedRef; 20 | -------------------------------------------------------------------------------- /src/components/modals/UserWasMuted/index.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | import { useTranslation } from 'react-i18next'; 3 | import ModalTitle from '../ModalTitle'; 4 | 5 | const UserWasMuted: FC = () => { 6 | const { t } = useTranslation(); 7 | return ( 8 | <> 9 | {t('Error')} 10 |

{t('You have been muted by an admin and cannot send messages.')}

11 | 12 | ); 13 | }; 14 | 15 | export default UserWasMuted; 16 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | NEXT_TELEMETRY_DISABLED=1 2 | NEXT_PUBLIC_APP_VERSION=$npm_package_version 3 | NEXT_PUBLIC_APP_GOOGLE_DRIVE_CLIENT_ID= 4 | NEXT_PUBLIC_APP_DROPBOX_CLIENT_ID= 5 | NEXT_PUBLIC_APP_GENERAL_CHAT_LINK=https://haven.xx.network/join 6 | # DFX CANISTER ENVIRONMENT VARIABLES 7 | DFX_VERSION='0.24.0' 8 | DFX_NETWORK='ic' 9 | CANISTER_ID_APP='' 10 | CANISTER_ID='' 11 | CANISTER_CANDID_PATH='/path/to/haven/.dfx/ic/canisters/app/assetstorage.did' 12 | # END DFX CANISTER ENVIRONMENT VARIABLES 13 | -------------------------------------------------------------------------------- /src/hooks/useChannelsStorageTag.ts: -------------------------------------------------------------------------------- 1 | import { JsonDecoder } from 'ts.data.json'; 2 | import useRemotelySynchedValue from './useRemotelySynchedValue'; 3 | import { makeDecoder } from '@utils/decoders'; 4 | 5 | const KEY = 'channels-storage-tag'; 6 | 7 | const useStorageTag = () => { 8 | const result = useRemotelySynchedValue(KEY, makeDecoder(JsonDecoder.string)); 9 | console.log('Ready storage tag value:', result.value); 10 | return result; 11 | }; 12 | 13 | export default useStorageTag; 14 | -------------------------------------------------------------------------------- /src/router.tsx: -------------------------------------------------------------------------------- 1 | import { createBrowserRouter } from 'react-router-dom'; 2 | import Home from './routes/Home'; 3 | import Join from './routes/Join'; 4 | import App from './App'; 5 | 6 | export const router = createBrowserRouter([ 7 | { 8 | path: '/', 9 | element: , 10 | children: [ 11 | { 12 | index: true, 13 | element: 14 | }, 15 | { 16 | path: 'join', 17 | element: 18 | } 19 | ] 20 | } 21 | ]); 22 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "chrome", 9 | "request": "launch", 10 | "name": "Launch Chrome against localhost", 11 | "url": "http://localhost:3000", 12 | "webRoot": "${workspaceFolder}" 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /src/utils/date.ts: -------------------------------------------------------------------------------- 1 | import dayjs from 'dayjs'; 2 | 3 | export const formatTimeAgo = (date: Date | number): string => { 4 | return dayjs(date).format('HH:mm'); 5 | }; 6 | 7 | export const formatDate = (key: string, timestamp?: string): string => { 8 | const date = dayjs(key); 9 | const today = dayjs(); 10 | 11 | if (date.isSame(today, 'day')) { 12 | return 'Today'; 13 | } 14 | if (date.isSame(today.subtract(1, 'day'), 'day')) { 15 | return 'Yesterday'; 16 | } 17 | return date.format('MMMM D, YYYY'); 18 | }; 19 | -------------------------------------------------------------------------------- /src/components/common/CloseButton.tsx: -------------------------------------------------------------------------------- 1 | import { FC, HTMLAttributes } from 'react'; 2 | import cn from 'classnames'; 3 | 4 | import X from '@components/icons/X'; 5 | 6 | const CloseButton: FC> = (props) => ( 7 | 16 | ); 17 | 18 | export default CloseButton; 19 | -------------------------------------------------------------------------------- /src/components/modals/JoinChannelSuccessView/JoinChannelSuccessView.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | import { useTranslation } from 'react-i18next'; 3 | 4 | const JoinChannelSuccessView: FC = () => { 5 | const { t } = useTranslation(); 6 | return ( 7 |
8 | 9 | {t('Awesome! You joined a new Haven Chat successfully.')} 10 | 11 |
12 | ); 13 | }; 14 | 15 | export default JoinChannelSuccessView; 16 | -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import { RouterProvider } from 'react-router-dom'; 4 | import { HelmetProvider } from 'react-helmet-async'; 5 | import { router } from './router'; 6 | import './i18n'; 7 | import './utils/extend-dayjs'; 8 | import './assets/css/globals.css'; 9 | 10 | ReactDOM.createRoot(document.getElementById('root')!).render( 11 | 12 | 13 | 14 | 15 | 16 | ); 17 | -------------------------------------------------------------------------------- /src/quill/EmojiBlot.ts: -------------------------------------------------------------------------------- 1 | import { EmbedBlot } from 'parchment'; 2 | import Quill from 'quill'; 3 | const Embed = Quill.import('blots/embed'); 4 | 5 | class EmojiBlot extends EmbedBlot { 6 | static blotName = 'emoji'; 7 | static tagName = 'span'; 8 | static className = 'emoji'; 9 | 10 | static create(value: string) { 11 | const node = super.create(value); 12 | node.textContent = value; 13 | return node; 14 | } 15 | 16 | static value(node: HTMLElement) { 17 | return node.textContent; 18 | } 19 | } 20 | 21 | export default EmojiBlot; 22 | -------------------------------------------------------------------------------- /src/components/modals/NetworkNotReadyView/NetworkNotReadyView.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | 3 | import cn from 'classnames'; 4 | import { useTranslation } from 'react-i18next'; 5 | 6 | const NetworkNotReadyView: FC = () => { 7 | const { t } = useTranslation(); 8 | return ( 9 |
10 |

11 | {t('The network is getting ready, please try again shortly.')} 12 |

13 |
14 | ); 15 | }; 16 | 17 | export default NetworkNotReadyView; 18 | -------------------------------------------------------------------------------- /src/components/common/FormError.tsx: -------------------------------------------------------------------------------- 1 | import type { WithChildren } from 'src/types'; 2 | import type { FC } from 'react'; 3 | import ErrorIcon from 'src/components/icons/Error'; 4 | 5 | const FormError: FC = ({ children }) => ( 6 |
10 | 11 | {children} 12 |
13 | ); 14 | 15 | export default FormError; 16 | -------------------------------------------------------------------------------- /cypress/support/e2e.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | import './commands'; 3 | import '@cypress/code-coverage/support'; 4 | 5 | if (Cypress.config('hideXHRInCommandLog')) { 6 | const app = window.top; 7 | 8 | if (app && !app.document.head.querySelector('[data-hide-command-log-request]')) { 9 | const style = app.document.createElement('style'); 10 | style.innerHTML = '.command-name-request, .command-name-xhr { display: none }'; 11 | style.setAttribute('data-hide-command-log-request', ''); 12 | 13 | app.document.head.appendChild(style); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/components/common/WarningComponent/index.tsx: -------------------------------------------------------------------------------- 1 | import { FC, type PropsWithChildren } from 'react'; 2 | 3 | const WarningComponent: FC = ({ children }) => { 4 | return ( 5 |
6 |

14 | {children} 15 |

16 |
17 | ); 18 | }; 19 | 20 | export default WarningComponent; 21 | -------------------------------------------------------------------------------- /src/components/common/index.ts: -------------------------------------------------------------------------------- 1 | export { default as WebAssemblyRunner } from './WebAssemblyRunner'; 2 | export { default as Button } from './Button'; 3 | export { default as RightSideBar } from './RightSideBar'; 4 | export { default as LeftSideBar } from './LeftSideBar'; 5 | export { default as Collapse } from './Collapse'; 6 | export { default as ChannelChat } from './ChannelChat'; 7 | export { default as Spinner } from './Spinner'; 8 | export { default as Loading } from './Loading'; 9 | export { default as ProgressBar } from './ProgressBar'; 10 | export { default as ImportCodeNameLoading } from './ImportCodeNameLoading'; 11 | -------------------------------------------------------------------------------- /src/store/identity/index.ts: -------------------------------------------------------------------------------- 1 | import type { Identity, IdentityState } from './types'; 2 | import { createSlice, PayloadAction } from '@reduxjs/toolkit'; 3 | 4 | const initialState: IdentityState = { 5 | identity: undefined 6 | }; 7 | 8 | const slice = createSlice({ 9 | name: 'identity', 10 | initialState, 11 | reducers: { 12 | set: (state: IdentityState, action: PayloadAction) => ({ 13 | identity: action.payload || state.identity 14 | }) 15 | } 16 | }); 17 | 18 | export default slice.reducer; 19 | 20 | export const { actions } = slice; 21 | 22 | export * as selectors from './selectors'; 23 | -------------------------------------------------------------------------------- /src/components/icons/ConnectingLine.tsx: -------------------------------------------------------------------------------- 1 | import { SVGProps } from 'react'; 2 | 3 | const ConnectingLine = (props: SVGProps) => { 4 | return ( 5 | 13 | 17 | 18 | 19 | ); 20 | }; 21 | 22 | export default ConnectingLine; 23 | -------------------------------------------------------------------------------- /src/layouts/DefaultLayout/ConnectingDimmer.tsx: -------------------------------------------------------------------------------- 1 | import { Spinner } from '@components/common'; 2 | import { NetworkStatus, useNetworkClient } from '@contexts/network-client-context'; 3 | import React from 'react'; 4 | 5 | const ConnectingDimmer = () => { 6 | const { networkStatus } = useNetworkClient(); 7 | 8 | return networkStatus === NetworkStatus.CONNECTING ? ( 9 |
10 | 11 |

Connecting to the network...

12 |
13 | ) : null; 14 | }; 15 | 16 | export default ConnectingDimmer; 17 | -------------------------------------------------------------------------------- /public/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/views/Register/Register.tsx: -------------------------------------------------------------------------------- 1 | import { FC, useCallback, useState } from 'react'; 2 | 3 | import Registration from '../Registration'; 4 | import CodeNameRegistration from '../CodeNameRegistration'; 5 | 6 | const Register: FC = () => { 7 | const [password, setPassword] = useState(); 8 | 9 | const onPasswordConfirmation = useCallback((pass: string) => { 10 | setPassword(pass); 11 | }, []); 12 | 13 | return password ? ( 14 | 15 | ) : ( 16 | 17 | ); 18 | }; 19 | 20 | export default Register; 21 | -------------------------------------------------------------------------------- /src/components/icons/MissedMessagesIcon/MissedMessagesIcon.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | import cn from 'classnames'; 3 | 4 | const MissedMessagesIcon: FC<{ muted?: boolean }> = ({ muted }) => { 5 | return ( 6 |
7 |
16 |
17 | ); 18 | }; 19 | 20 | export default MissedMessagesIcon; 21 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | import { configureStore } from '@reduxjs/toolkit'; 2 | import channelsReducer from './channels'; 3 | import messagesReducer from './messages'; 4 | import identityReducer from './identity'; 5 | import dmsReducer from './dms'; 6 | import appReducer from './app'; 7 | 8 | const store = configureStore({ 9 | reducer: { 10 | channels: channelsReducer, 11 | messages: messagesReducer, 12 | identity: identityReducer, 13 | dms: dmsReducer, 14 | app: appReducer 15 | } 16 | }); 17 | 18 | export type RootState = ReturnType; 19 | export type AppDispatch = typeof store.dispatch; 20 | 21 | export default store; 22 | -------------------------------------------------------------------------------- /src/components/common/LeftHeader/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | 3 | import User from './User'; 4 | import SidebarControls from './SidebarControls'; 5 | 6 | type Props = { 7 | className?: string; 8 | }; 9 | 10 | const Header: FC = ({ className }) => { 11 | return ( 12 |
22 | 23 | 24 |
25 | ); 26 | }; 27 | 28 | export default Header; 29 | -------------------------------------------------------------------------------- /src/components/icons/Checkmark.tsx: -------------------------------------------------------------------------------- 1 | import { SVGProps } from 'react'; 2 | 3 | const Checkmark = (props: SVGProps) => { 4 | return ( 5 | 12 | 13 | 17 | 18 | 19 | ); 20 | }; 21 | 22 | export default Checkmark; 23 | -------------------------------------------------------------------------------- /src/hooks/useInput.ts: -------------------------------------------------------------------------------- 1 | import { useState, useCallback, ChangeEvent } from 'react'; 2 | 3 | type OnChangeType = (e: ChangeEvent) => void; 4 | type UseInput = ( 5 | initialValue?: string 6 | ) => [string, OnChangeType, { set: (value: string) => void; touched: boolean }]; 7 | 8 | const useInput: UseInput = (initValue = '') => { 9 | const [value, set] = useState(initValue); 10 | const [touched, setTouched] = useState(false); 11 | 12 | const handler = useCallback((e) => { 13 | setTouched(true); 14 | set(e.target.value); 15 | }, []); 16 | 17 | return [value, handler, { set, touched }]; 18 | }; 19 | 20 | export default useInput; 21 | -------------------------------------------------------------------------------- /src/components/modals/ImportAccountView/index.tsx: -------------------------------------------------------------------------------- 1 | import type { IdentityVariables } from './types'; 2 | 3 | import React, { FC } from 'react'; 4 | 5 | import { useUI } from '@contexts/ui-context'; 6 | import Modal from 'src/components/modals/Modal'; 7 | import ImportAccountForm from './ImportAccountForm'; 8 | 9 | type Props = { 10 | onSubmit: (identityVariables: IdentityVariables) => Promise; 11 | }; 12 | 13 | const ImportAccountModal: FC = ({ onSubmit }) => { 14 | const { closeModal } = useUI(); 15 | 16 | return ( 17 | 18 | 19 | 20 | ); 21 | }; 22 | 23 | export default ImportAccountModal; 24 | -------------------------------------------------------------------------------- /src/components/icons/X.tsx: -------------------------------------------------------------------------------- 1 | import { SVGProps } from 'react'; 2 | 3 | const X = (props: SVGProps) => { 4 | return ( 5 | 12 | 16 | 17 | ); 18 | }; 19 | 20 | export default X; 21 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | image: node:16 2 | 3 | stages: 4 | - container 5 | - deploy 6 | 7 | build_image: 8 | image: docker:latest 9 | stage: container 10 | services: 11 | - docker:dind 12 | variables: 13 | DOCKER_TLS_CERTDIR: '/certs' 14 | script: 15 | - docker info 16 | - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY 17 | - docker build --build-arg SPEAKEASY_VER=$CI_COMMIT_REF_NAME --no-cache -t docker-registry.xx.network/elixxir/speakeasy-web:$CI_COMMIT_REF_NAME . 18 | - docker push docker-registry.xx.network/elixxir/speakeasy-web:$CI_COMMIT_REF_NAME 19 | only: 20 | - staging 21 | - dev 22 | - backend-dev 23 | - tags 24 | tags: 25 | - dind 26 | -------------------------------------------------------------------------------- /src/components/icons/DoubleLeftArrows.tsx: -------------------------------------------------------------------------------- 1 | import { SVGProps } from 'react'; 2 | 3 | const DoubleLeftArrows = (props: SVGProps) => ( 4 | 12 | 16 | 20 | 21 | ); 22 | 23 | export default DoubleLeftArrows; 24 | -------------------------------------------------------------------------------- /src/types/json.ts: -------------------------------------------------------------------------------- 1 | export type ShareURLJSON = { 2 | url: string; 3 | password?: string; 4 | }; 5 | 6 | export type IdentityJSON = { 7 | pubkey: string; 8 | codename: string; 9 | color: string; 10 | extension: string; 11 | codeset: number; 12 | }; 13 | 14 | export type ChannelJSON = { 15 | receptionId?: string; 16 | channelId?: string; 17 | name: string; 18 | description: string; 19 | }; 20 | 21 | export type VersionJSON = { 22 | current: string; 23 | updated: boolean; 24 | old: string; 25 | }; 26 | 27 | export type IsReadyInfoJSON = { 28 | isReady: boolean; 29 | howClose: number; 30 | }; 31 | 32 | export type MessageReceivedJSON = { 33 | uuid: number; 34 | channelId: string; 35 | update: boolean; 36 | }; 37 | -------------------------------------------------------------------------------- /src/hooks/useNetworkTrackPeriod.ts: -------------------------------------------------------------------------------- 1 | import useLocalStorage from './useLocalStorage'; 2 | import { useCallback } from 'react'; 3 | import { FAST_MODE_TRACKING_PERIOD_MS, SLOW_MODE_TRACKING_PERIOD_MS } from 'src/constants'; 4 | 5 | const useTrackNetworkPeriod = () => { 6 | const [trackingMode, setMode] = useLocalStorage<'slow' | 'fast'>('TRACK_NETWORK_PERIOD', 'fast'); 7 | 8 | const toggle = useCallback(() => { 9 | setMode(trackingMode === 'fast' ? 'slow' : 'fast'); 10 | }, [trackingMode, setMode]); 11 | 12 | return { 13 | trackingMode, 14 | trackingMs: 15 | trackingMode === 'slow' ? SLOW_MODE_TRACKING_PERIOD_MS : FAST_MODE_TRACKING_PERIOD_MS, 16 | toggle 17 | }; 18 | }; 19 | 20 | export default useTrackNetworkPeriod; 21 | -------------------------------------------------------------------------------- /src/components/common/Loading/Loading.tsx: -------------------------------------------------------------------------------- 1 | import type { WithChildren } from 'src/types'; 2 | import type { FC } from 'react'; 3 | import { useTranslation } from 'react-i18next'; 4 | import { Spinner } from 'src/components/common'; 5 | 6 | type Props = WithChildren; 7 | 8 | const Loading: FC = ({ children }) => { 9 | const { t } = useTranslation(); 10 | return ( 11 |
12 |
13 | {children ? children : } 14 |
{t('Loading...')}
15 |
16 |
17 | ); 18 | }; 19 | 20 | export default Loading; 21 | -------------------------------------------------------------------------------- /src/store/app/types.ts: -------------------------------------------------------------------------------- 1 | import type { ChannelId } from '../channels/types'; 2 | import { ConversationId } from '../dms/types'; 3 | import { MessageId } from '../messages/types'; 4 | 5 | export type AppState = { 6 | selectedChannelIdOrConversationId: string | null; 7 | selectedUserPubkey: string | null; 8 | messageDraftsByChannelId: Record; 9 | channelsSearch: string; 10 | dmsSearch?: string; 11 | contributorsSearch: string; 12 | channelFavorites: string[]; 13 | missedMessages?: Record; 14 | replyingToMessageId?: string; 15 | highlightedMessageId?: string; 16 | }; 17 | 18 | declare module 'src/store/types' { 19 | interface RootState { 20 | app: AppState; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /local.Dockerfile: -------------------------------------------------------------------------------- 1 | # Build stage 2 | FROM node:20 AS builder 3 | WORKDIR /haven-web 4 | 5 | # Copy only files needed for installation 6 | COPY package*.json ./ 7 | COPY next.config.js ./ 8 | COPY tsconfig*.json ./ 9 | 10 | # Install dependencies 11 | RUN npm ci 12 | 13 | # Copy sourcecode 14 | COPY . . 15 | 16 | # Build the application 17 | RUN npm run build 18 | 19 | # Lightweight production image 20 | FROM node:20-slim 21 | ENV NODE_ENV=production 22 | WORKDIR /haven-web 23 | 24 | # Install only production dependencies 25 | COPY package*.json ./ 26 | RUN npm ci --omit=dev 27 | 28 | # Copy only the built output from the builder stage 29 | COPY --from=builder /haven-web/.next .next 30 | COPY --from=builder /haven-web/out out 31 | 32 | ENTRYPOINT [ "npm", "run", "start" ] 33 | -------------------------------------------------------------------------------- /src/components/icons/DoubleRightArrows.tsx: -------------------------------------------------------------------------------- 1 | import { SVGProps } from 'react'; 2 | 3 | const DoubleRightyArrows = (props: SVGProps) => ( 4 | 13 | 17 | 21 | 22 | ); 23 | 24 | export default DoubleRightyArrows; 25 | -------------------------------------------------------------------------------- /src/components/icons/Dev.tsx: -------------------------------------------------------------------------------- 1 | import { SVGProps } from 'react'; 2 | 3 | const Dev = (props: SVGProps) => { 4 | return ( 5 | 6 | 7 | 8 | ); 9 | }; 10 | 11 | export default Dev; 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | .env 31 | .dfx/ 32 | 33 | # vercel 34 | .vercel 35 | 36 | # typescript 37 | *.tsbuildinfo 38 | next-env.d.ts 39 | 40 | # Ignore Jetbrains IDE folder 41 | .idea/* 42 | 43 | # Ignore emacs temporary files 44 | *~ 45 | *# 46 | 47 | # Vite files 48 | dist/ 49 | 50 | # Bun 51 | bun.lockb 52 | 53 | 54 | # Local Netlify folder 55 | .netlify 56 | -------------------------------------------------------------------------------- /src/components/common/Input/index.tsx: -------------------------------------------------------------------------------- 1 | import { FC, InputHTMLAttributes } from 'react'; 2 | 3 | type Size = 'sm' | 'md' | 'lg'; 4 | 5 | export type Props = { 6 | size?: Size; 7 | } & Omit, 'size'>; 8 | 9 | const sizeMap: Record = { 10 | sm: 'h-8', 11 | md: 'h-10', 12 | lg: 'h-14' 13 | }; 14 | 15 | const Input: FC = ({ size = 'md', ...props }) => ( 16 | 30 | ); 31 | 32 | export default Input; 33 | -------------------------------------------------------------------------------- /src/components/common/ProgressBar/ProgressBar.tsx: -------------------------------------------------------------------------------- 1 | const ProgressBar = (props: { completed: number }) => { 2 | const { completed } = props; 3 | const normalizedCompleted = completed > 100 ? 100 : completed; 4 | 5 | return ( 6 |
7 |
11 | 12 | 13 | {`${normalizedCompleted}%`} 14 | 15 |
16 |
17 | ); 18 | }; 19 | 20 | export default ProgressBar; 21 | -------------------------------------------------------------------------------- /src/components/common/ChannelChat/SendButton/index.tsx: -------------------------------------------------------------------------------- 1 | import { ButtonHTMLAttributes, FC } from 'react'; 2 | import Send from '@components/icons/Send'; 3 | 4 | const SendButton: FC> = (props) => { 5 | return ( 6 | 23 | ); 24 | }; 25 | 26 | export default SendButton; 27 | -------------------------------------------------------------------------------- /src/components/icons/NetworkStatusIcon/NetworkStatusIcon.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | import cn from 'classnames'; 3 | import { useNetworkClient } from 'src/contexts/network-client-context'; 4 | 5 | const NetworkStatusIcon: FC = () => { 6 | const { isNetworkHealthy } = useNetworkClient(); 7 | 8 | if (typeof isNetworkHealthy === 'undefined') { 9 | return null; 10 | } else { 11 | return ( 12 |
13 |
19 |
20 | ); 21 | } 22 | }; 23 | 24 | export default NetworkStatusIcon; 25 | -------------------------------------------------------------------------------- /src/components/modals/NetworkNotReadyView/index.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | import { useTranslation } from 'react-i18next'; 3 | import { Button } from 'src/components/common'; 4 | import { useUI } from 'src/contexts/ui-context'; 5 | import ModalTitle from '../ModalTitle'; 6 | import React from 'react'; 7 | 8 | const NetworkNotReadyView: FC = () => { 9 | const { t } = useTranslation(); 10 | const { closeModal } = useUI(); 11 | 12 | return ( 13 | <> 14 | {t('Network Not Ready')} 15 |

16 | {t('The network is not ready to send messages yet. Please try again in a few seconds.')} 17 |

18 | 21 | 22 | ); 23 | }; 24 | 25 | export default NetworkNotReadyView; 26 | -------------------------------------------------------------------------------- /src/components/common/Badge.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, HTMLAttributes } from 'react'; 2 | 3 | type Props = HTMLAttributes & { color?: 'gold' | 'blue' | 'grey' }; 4 | 5 | const Badge: FC = ({ children, color = 'blue', ...props }) => { 6 | const colorClasses = { 7 | blue: 'text-[var(--blue)] border-[var(--blue)]', 8 | gold: 'text-[var(--primary)] border-[var(--primary)]', 9 | grey: 'text-[var(--charcoal-2)] border-[var(--charcoal-2)]' 10 | }; 11 | 12 | return ( 13 | 23 | {children} 24 | 25 | ); 26 | }; 27 | 28 | export default Badge; 29 | -------------------------------------------------------------------------------- /src/components/common/ErrorBoundary.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ErrorBoundary as ReactErrorBoundary } from 'react-error-boundary'; 3 | import { Helmet } from 'react-helmet-async'; 4 | 5 | type Props = { 6 | children: React.ReactNode; 7 | }; 8 | 9 | const ErrorFallback = ({ error }: { error: Error }) => { 10 | return ( 11 | <> 12 | 13 | Error - Haven Web 14 | 15 |
16 |

Something went wrong:

17 |
{error.message}
18 |
19 | 20 | ); 21 | }; 22 | 23 | const ErrorBoundary: React.FC = ({ children }) => { 24 | return ( 25 | window.location.reload()}> 26 | {children} 27 | 28 | ); 29 | }; 30 | 31 | export default ErrorBoundary; 32 | -------------------------------------------------------------------------------- /src/hooks/useCopyToClipboard.ts: -------------------------------------------------------------------------------- 1 | import copy from 'copy-to-clipboard'; 2 | import { useCallback, useEffect, useState } from 'react'; 3 | 4 | type CopyResponse = [boolean, (copy: string) => void, string]; 5 | 6 | export default function useCopyClipboard(timeout = 1000): CopyResponse { 7 | const [isCopied, setIsCopied] = useState(false); 8 | const [copiedText, setCopiedText] = useState(''); 9 | 10 | const staticCopy = useCallback((text: string) => { 11 | const didCopy = copy(text); 12 | setCopiedText(text); 13 | setIsCopied(didCopy); 14 | }, []); 15 | 16 | useEffect(() => { 17 | if (isCopied) { 18 | const hide = setTimeout(() => { 19 | setIsCopied(false); 20 | }, timeout); 21 | 22 | return () => { 23 | clearTimeout(hide); 24 | }; 25 | } 26 | return undefined; 27 | }, [isCopied, setIsCopied, timeout]); 28 | 29 | return [isCopied, staticCopy, copiedText]; 30 | } 31 | -------------------------------------------------------------------------------- /src/components/views/SettingsViews/index.tsx: -------------------------------------------------------------------------------- 1 | import type { SettingsView } from 'src/types/ui'; 2 | 3 | import { FC } from 'react'; 4 | 5 | import { useUI } from '@contexts/ui-context'; 6 | 7 | import ExportCodenameView from './ExportCodename'; 8 | import NotificationsView from './NotificationsView'; 9 | import DeveloperOptionsView from './DeveloperOptions'; 10 | import LogOutView from './LogOut'; 11 | 12 | const NOOP = () => null; 13 | 14 | const views: Partial> = { 15 | notifications: NotificationsView, 16 | logout: LogOutView, 17 | 'export-codename': ExportCodenameView, 18 | dev: DeveloperOptionsView 19 | }; 20 | 21 | const Settings = () => { 22 | const { settingsView } = useUI(); 23 | 24 | const View = views[settingsView] ?? NOOP; 25 | return ( 26 |
27 | 28 |
29 | ); 30 | }; 31 | 32 | export default Settings; 33 | -------------------------------------------------------------------------------- /src/components/common/LeftSideBar/LeftSideBar.tsx: -------------------------------------------------------------------------------- 1 | import type { FC } from 'react'; 2 | import cn from 'classnames'; 3 | 4 | import { WithChildren } from 'src/types'; 5 | import { LeftSidebarView } from 'src/types/ui'; 6 | import DMs from '../DMs'; 7 | import Spaces from '../Spaces'; 8 | import SettingsMenu from '../SettingsMenu'; 9 | import { useUI } from 'src/contexts/ui-context'; 10 | 11 | const views: Record = { 12 | dms: DMs, 13 | spaces: Spaces, 14 | settings: SettingsMenu 15 | }; 16 | 17 | const LeftSideBar: FC = ({ className }) => { 18 | const { leftSidebarView } = useUI(); 19 | 20 | const View = views[leftSidebarView] ?? (() => null); 21 | return ( 22 |
26 | 27 |
28 | ); 29 | }; 30 | 31 | export default LeftSideBar; 32 | -------------------------------------------------------------------------------- /src/components/icons/Warning.tsx: -------------------------------------------------------------------------------- 1 | import { SVGProps } from 'react'; 2 | 3 | const Warning = (props: SVGProps) => { 4 | return ( 5 | 12 | 16 | 17 | ); 18 | }; 19 | 20 | export default Warning; 21 | -------------------------------------------------------------------------------- /src/components/icons/Plus.tsx: -------------------------------------------------------------------------------- 1 | import { SVGProps } from 'react'; 2 | 3 | const Plus = (props: SVGProps) => { 4 | return ( 5 | 13 | 14 | 21 | 22 | 23 | ); 24 | }; 25 | 26 | export default Plus; 27 | -------------------------------------------------------------------------------- /src/components/icons/Ellipsis.tsx: -------------------------------------------------------------------------------- 1 | import { SVGProps } from 'react'; 2 | 3 | const Ellipsis = (props: SVGProps) => { 4 | return ( 5 | 13 | 14 | 21 | 22 | 23 | ); 24 | }; 25 | 26 | export default Ellipsis; 27 | -------------------------------------------------------------------------------- /src/assets/images/logo.svg: -------------------------------------------------------------------------------- 1 | 8 | 13 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "target": "ESNext", 5 | "lib": ["dom", "dom.iterable", "esnext", "es2018.regexp"], 6 | "allowJs": true, 7 | "skipLibCheck": true, 8 | "strict": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "noEmit": true, 11 | "esModuleInterop": true, 12 | "module": "esnext", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "jsx": "preserve", 17 | "incremental": true, 18 | "allowSyntheticDefaultImports": true, 19 | "paths": { 20 | "@/*": ["src/*"], 21 | "src/*": ["src/*"], 22 | "@contexts/*": ["src/contexts/*"], 23 | "@components/*": ["src/components/*"], 24 | "@assets/*": ["src/assets/*"], 25 | "@utils/*": ["src/utils/*"] 26 | } 27 | }, 28 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "src/quill/constants.js"], 29 | "exclude": ["node_modules", "src/declarations/**/*.js", "src/declarations/**/*.ts"] 30 | } 31 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | command = "bun run build" 3 | publish = "dist" 4 | 5 | [dev] 6 | command = "bun run dev" 7 | port = 3001 8 | targetPort = 3000 9 | publish = "dist" 10 | framework = "vite" 11 | autoLaunch = true 12 | 13 | # Handle React Router 14 | [[redirects]] 15 | from = "/*" 16 | to = "/index.html" 17 | status = 200 18 | 19 | # Security headers 20 | [[headers]] 21 | for = "/*" 22 | [headers.values] 23 | X-Frame-Options = "DENY" 24 | X-XSS-Protection = "1; mode=block" 25 | X-Content-Type-Options = "nosniff" 26 | Referrer-Policy = "strict-origin-when-cross-origin" 27 | Content-Security-Policy = "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' blob:; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https:; connect-src 'self' https:; worker-src 'self' blob:; child-src 'self' blob:;" 28 | 29 | # Cache control for static assets 30 | [[headers]] 31 | for = "/assets/*" 32 | [headers.values] 33 | Cache-Control = "public, max-age=31536000, immutable" 34 | -------------------------------------------------------------------------------- /src/store/channels/types.ts: -------------------------------------------------------------------------------- 1 | import { ChannelNotificationLevel, NotificationStatus } from 'src/types'; 2 | import { PrivacyLevel } from 'src/types'; 3 | export { PrivacyLevel }; 4 | 5 | export type Channel = { 6 | name: string; 7 | id: string; 8 | description: string; 9 | isAdmin: boolean; 10 | privacyLevel: PrivacyLevel | null; 11 | prettyPrint?: string; 12 | }; 13 | 14 | export type ChannelId = Channel['id']; 15 | 16 | export type ChannelsState = { 17 | byId: Record; 18 | sortedChannels: Array; 19 | currentPages: Record; 20 | nicknames: Record; 21 | mutedUsersByChannelId: Record; 22 | notificationLevels: Record; 23 | notificationStatuses: Record; 24 | dmsEnabled: Record; 25 | }; 26 | 27 | declare module 'src/store/types' { 28 | interface RootState { 29 | channels: ChannelsState; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/i18n/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2017-2022 @polkadot/react-components authors & contributors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import i18next from 'i18next'; 5 | import LanguageDetector from 'i18next-browser-languagedetector'; 6 | import { initReactI18next } from 'react-i18next'; 7 | 8 | import Backend from './backend'; 9 | 10 | i18next 11 | .use(LanguageDetector) 12 | .use(initReactI18next) 13 | .use(Backend) 14 | .init({ 15 | backend: {}, 16 | debug: false, 17 | detection: { 18 | order: ['i18nLangDetector', 'navigator'] 19 | }, 20 | fallbackLng: false, 21 | interpolation: { 22 | escapeValue: false, 23 | prefix: '{{', 24 | suffix: '}}' 25 | }, 26 | keySeparator: false, 27 | load: 'languageOnly', 28 | nsSeparator: false, 29 | react: { 30 | useSuspense: false 31 | }, 32 | returnEmptyString: false, 33 | returnNull: false 34 | }) 35 | .catch((error: Error): void => console.error('i18n: failure', error)); 36 | 37 | export default i18next; 38 | -------------------------------------------------------------------------------- /src/hooks/useAccountSync.ts: -------------------------------------------------------------------------------- 1 | import { ACCOUNT_SYNC, ACCOUNT_SYNC_SERVICE } from 'src/constants'; 2 | import useLocalStorage from './useLocalStorage'; 3 | import { useMemo } from 'react'; 4 | 5 | export enum AccountSyncStatus { 6 | NotSynced = 'NotSynced', 7 | Synced = 'Synced', 8 | Ignore = 'Ignored' 9 | } 10 | 11 | export enum AccountSyncService { 12 | None = 'None', 13 | Google = 'Google', 14 | Dropbox = 'Dropbox' 15 | } 16 | 17 | const NOT_SYNCED_STATUSES = [AccountSyncStatus.NotSynced, AccountSyncStatus.Ignore]; 18 | 19 | const useAccountSync = () => { 20 | const [status, setStatus] = useLocalStorage(ACCOUNT_SYNC, AccountSyncStatus.NotSynced); 21 | const [service, setService] = useLocalStorage(ACCOUNT_SYNC_SERVICE, AccountSyncService.None); 22 | 23 | const isSynced = useMemo( 24 | () => status !== null && !NOT_SYNCED_STATUSES.includes(status), 25 | [status] 26 | ); 27 | 28 | return { 29 | status, 30 | setStatus, 31 | service, 32 | setService, 33 | isSynced 34 | }; 35 | }; 36 | 37 | export default useAccountSync; 38 | -------------------------------------------------------------------------------- /src/contexts/sound-context.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, FC, useContext, useState, useCallback } from 'react'; 2 | import { useRemotelySynchedString } from 'src/hooks/useRemotelySynchedValue'; 3 | import NotificationSound from '@components/common/NotificationSound'; 4 | 5 | type SoundContextType = { 6 | playNotification: (() => void) | null; 7 | }; 8 | 9 | const SoundContext = createContext({ playNotification: null }); 10 | 11 | export const SoundProvider: FC<{ children: React.ReactNode }> = ({ children }) => { 12 | const [playNotification, setPlayNotification] = useState<(() => void) | null>(null); 13 | const { value: notificationSound } = useRemotelySynchedString( 14 | 'notification-sound', 15 | '/sounds/notification.mp3' 16 | ); 17 | 18 | return ( 19 | 20 | 21 | {children} 22 | 23 | ); 24 | }; 25 | 26 | export const useSound = () => useContext(SoundContext); 27 | -------------------------------------------------------------------------------- /src/components/icons/Close.tsx: -------------------------------------------------------------------------------- 1 | import { SVGProps } from 'react'; 2 | 3 | const Close = (props: SVGProps) => ( 4 | 13 | 17 | 21 | 22 | ); 23 | 24 | export default Close; 25 | -------------------------------------------------------------------------------- /src/components/common/ImportCodeNameLoading/ImportCodeNameLoading.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | import { useTranslation } from 'react-i18next'; 3 | 4 | import { Spinner } from 'src/components/common'; 5 | import { ProgressBar } from 'src/components/common'; 6 | 7 | type Props = { 8 | readyProgress: number; 9 | fullscreen?: boolean; 10 | }; 11 | 12 | const ImportCodeNameLoading: FC = ({ readyProgress }) => { 13 | const { t } = useTranslation(); 14 | return ( 15 |
16 | 17 | 18 |
19 |
20 | {t('Securely setting up your codename. This could take a few minutes.')} 21 |
22 |
23 | {t('Please do not close this page - your codename may be lost')} 24 |
25 |
26 |
27 | ); 28 | }; 29 | 30 | export default ImportCodeNameLoading; 31 | -------------------------------------------------------------------------------- /src/hooks/useAsync.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useState } from 'react'; 2 | 3 | type AsyncStatus = 'idle' | 'pending' | 'success' | 'error'; 4 | 5 | const useAsync = Promise>(asyncFunction: T) => { 6 | const [status, setStatus] = useState('idle'); 7 | const [value, setValue] = useState>>(); 8 | const [error, setError] = useState(null); 9 | 10 | const execute = useCallback( 11 | (...args: Parameters) => { 12 | setStatus('pending'); 13 | setValue(undefined); 14 | setError(null); 15 | 16 | return asyncFunction(...args) 17 | .then((response: Awaited>) => { 18 | setValue(response); 19 | setStatus('success'); 20 | return response; 21 | }) 22 | .catch((err) => { 23 | setError((err as Error).message || err); 24 | setStatus('error'); 25 | }); 26 | }, 27 | [asyncFunction] 28 | ); 29 | 30 | return { execute, status, value, error }; 31 | }; 32 | 33 | export default useAsync; 34 | -------------------------------------------------------------------------------- /src/components/common/ChannelChat/PinnedMessage/index.tsx: -------------------------------------------------------------------------------- 1 | import type { FC } from 'react'; 2 | 3 | import { Pin } from 'src/components/icons'; 4 | import cn from 'classnames'; 5 | 6 | import * as messages from 'src/store/messages'; 7 | import { useAppSelector } from 'src/store/hooks'; 8 | import ChatMessage from '../ChatMessage/ChatMessage'; 9 | 10 | const PinnedMessage: FC = () => { 11 | const pinnedMessages = useAppSelector(messages.selectors.currentPinnedMessages); 12 | 13 | return pinnedMessages && pinnedMessages.length > 0 ? ( 14 |
19 | 20 |
21 | 27 |
28 |
29 | ) : null; 30 | }; 31 | 32 | export default PinnedMessage; 33 | -------------------------------------------------------------------------------- /src/hooks/useChannelFavorites.ts: -------------------------------------------------------------------------------- 1 | import { useCallback } from 'react'; 2 | 3 | import { channelFavoritesDecoder } from '@utils/decoders'; 4 | import useRemotelySynchedValue from './useRemotelySynchedValue'; 5 | 6 | const KEY = 'channel-favorites'; 7 | 8 | const useChannelFavorites = () => { 9 | const { 10 | loading, 11 | set, 12 | value: favorites = [] 13 | } = useRemotelySynchedValue(KEY, channelFavoritesDecoder); 14 | 15 | const toggleFavorite = useCallback( 16 | (channelId: string) => { 17 | if (!loading) { 18 | set( 19 | favorites.includes(channelId) 20 | ? favorites.filter((id) => id !== channelId) 21 | : favorites.concat(channelId) 22 | ); 23 | } 24 | }, 25 | [favorites, loading, set] 26 | ); 27 | 28 | const isFavorite = useCallback( 29 | (channelId?: string | null) => channelId && favorites.includes(channelId), 30 | [favorites] 31 | ); 32 | 33 | return { 34 | loading, 35 | favorites, 36 | toggle: toggleFavorite, 37 | isFavorite 38 | }; 39 | }; 40 | 41 | export default useChannelFavorites; 42 | -------------------------------------------------------------------------------- /src/components/common/SearchInput.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | import { useTranslation } from 'react-i18next'; 3 | import Input, { Props } from 'src/components/common/Input'; 4 | 5 | const SearchInput: FC = ({ className, ...props }) => { 6 | const { t } = useTranslation(); 7 | return ( 8 |
9 | 10 |
11 | 26 |
27 |
28 | ); 29 | }; 30 | 31 | export default SearchInput; 32 | -------------------------------------------------------------------------------- /src/components/modals/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './Modal'; 2 | export { default as CreateChannelView } from './CreateChannelView'; 3 | export { default as ExportAdminKeys } from './ExportAdminKeys'; 4 | export { default as JoinChannelView } from './JoinChannelView'; 5 | export { default as ClaimAdminKeys } from './ClaimAdminKeys'; 6 | export { default as ShareChannelView } from './ShareChannelView'; 7 | export { default as LeaveChannelConfirmationView } from './LeaveChannelConfirmationView'; 8 | export { default as NickNameSetView } from './NickNameSetView'; 9 | export { default as ChannelSettingsView } from './ChannelSettingsView'; 10 | export { default as ExportCodenameView } from './ExportCodenameView'; 11 | export { default as ImportAccountView } from './ImportAccountView'; 12 | export { default as NetworkNotReadyView } from './NetworkNotReadyView'; 13 | export { default as JoinChannelSuccessView } from './JoinChannelSuccessView'; 14 | export { default as LogoutView } from './LogoutView'; 15 | export { default as UserWasMuted } from './UserWasMuted'; 16 | export { default as ViewPinnedMessages } from './ViewPinnedMessages'; 17 | -------------------------------------------------------------------------------- /src/components/icons/Send.tsx: -------------------------------------------------------------------------------- 1 | import { SVGProps } from 'react'; 2 | 3 | const Send = (props: SVGProps) => { 4 | return ( 5 | 13 | 14 | 21 | 22 | 23 | ); 24 | }; 25 | 26 | export default Send; 27 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Build stage 2 | FROM node:20 AS builder 3 | WORKDIR /haven-web 4 | 5 | ARG SPEAKEASY_VER 6 | ENV SPEAKEASY_VER=$SPEAKEASY_VER 7 | 8 | # Install git 9 | RUN apt-get update && apt-get upgrade -y && apt-get install -y git 10 | 11 | # Clone repo from git.xx.network 12 | RUN git clone --depth=1 --branch $SPEAKEASY_VER https://git.xx.network/elixxir/speakeasy-web.git /haven-web 13 | 14 | # Install dependencies 15 | # Check if package-lock.json exists and run appropriate install command 16 | RUN [ -f package-lock.json ] && npm ci || npm install 17 | 18 | # Build the application 19 | RUN npm run build 20 | 21 | # Lightweight production image 22 | FROM node:20-slim 23 | ENV NODE_ENV=production 24 | WORKDIR /haven-web 25 | 26 | # Install only production dependencies 27 | COPY package*.json ./ 28 | # Check if package-lock.json exists and run appropriate install command 29 | RUN [ -f package-lock.json ] && npm ci --omit=dev || npm install --omit=dev 30 | 31 | # Copy only the built output from the builder stage 32 | COPY --from=builder /haven-web/.next .next 33 | COPY --from=builder /haven-web/out out 34 | 35 | ENTRYPOINT [ "npm", "run", "start" ] 36 | -------------------------------------------------------------------------------- /src/components/common/CheckboxToggle/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { InputHTMLAttributes, FC } from 'react'; 2 | 3 | type Props = InputHTMLAttributes; 4 | 5 | const CheckboxToggle: FC = (props) => ( 6 |