├── .expo-shared ├── README.md └── assets.json ├── .gitignore ├── .prettierrc.js ├── App.tsx ├── README.md ├── app ├── rootReducer.ts ├── store.ts └── types.ts ├── assets └── icon.png ├── babel.config.js ├── components ├── Avatar.tsx ├── Button.tsx ├── CabalList.tsx ├── ChannelDetailPanel.tsx ├── ChannelHeader.tsx ├── ChannelListItem.tsx ├── HelpText.tsx ├── Input.tsx ├── MenuButton.tsx ├── Message.tsx ├── MessageComposer.tsx ├── MessageContent.tsx ├── MessageList.tsx ├── PanelHeader.tsx ├── PanelSection.tsx ├── SectionHeaderText.tsx ├── Sidebar.tsx ├── SidebarHeader.tsx ├── SidebarList.tsx └── UserProfilePanel.tsx ├── definitions.d.ts ├── features ├── cabals │ ├── cabalsSlice.ts │ └── messagesSlice.ts └── themes │ └── themesSlice.ts ├── hooks ├── useCabal.ts └── useIsMobile.tsx ├── lib ├── CabalProvider.tsx ├── hooks │ ├── useCabal.ts │ ├── useChannel.ts │ ├── useMessage.ts │ └── useUsers.js └── index.tsx ├── package.json ├── screens ├── AddCabalScreen.tsx ├── AppSettingsScreen.tsx ├── CabalSettingsScreen.tsx ├── ChannelBrowserScreen.tsx ├── ChannelDetailScreen.tsx ├── ChannelScreen.tsx ├── HomeScreen.test.js ├── HomeScreen.tsx ├── ThemeEditorScreen.tsx └── UserProfileScreen.tsx ├── tsconfig.json ├── utils ├── Themes.ts ├── Translations.ts ├── cabal-main-ipc.js ├── cabal-render-ipc.js ├── cabal-websocket-client.js ├── fakeData.ts ├── helpers.js └── textInputCommands.js ├── web ├── cabal-client-bundle.js └── index.html └── yarn.lock /.expo-shared/README.md: -------------------------------------------------------------------------------- 1 | > Why do I have a folder named ".expo-shared" in my project? 2 | 3 | The ".expo-shared" folder is created when running commands that produce state that is intended to be shared with all developers on the project. For example, "npx expo-optimize". 4 | 5 | > What does the "assets.json" file contain? 6 | 7 | The "assets.json" file describes the assets that have been optimized through "expo-optimize" and do not need to be processed again. 8 | 9 | > Should I commit the ".expo-shared" folder? 10 | 11 | Yes, you should share the ".expo-shared" folder with your collaborators. 12 | -------------------------------------------------------------------------------- /.expo-shared/assets.json: -------------------------------------------------------------------------------- 1 | { 2 | "12a281686fb9baadacbab8046ffb6213fe54e319a6b62624b1a816e0baa716a0": true 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # Xcode 6 | # 7 | build/ 8 | *.pbxuser 9 | !default.pbxuser 10 | *.mode1v3 11 | !default.mode1v3 12 | *.mode2v3 13 | !default.mode2v3 14 | *.perspectivev3 15 | !default.perspectivev3 16 | xcuserdata 17 | *.xccheckout 18 | *.moved-aside 19 | DerivedData 20 | *.hmap 21 | *.ipa 22 | *.xcuserstate 23 | project.xcworkspace 24 | 25 | # Android/IntelliJ 26 | # 27 | build/ 28 | .idea 29 | .gradle 30 | local.properties 31 | *.iml 32 | 33 | # node.js 34 | # 35 | node_modules/ 36 | npm-debug.log 37 | yarn-error.log 38 | 39 | # BUCK 40 | buck-out/ 41 | \.buckd/ 42 | *.keystore 43 | 44 | # fastlane 45 | # 46 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 47 | # screenshots whenever they are needed. 48 | # For more information about the recommended setup visit: 49 | # https://docs.fastlane.tools/best-practices/source-control/ 50 | 51 | */fastlane/report.xml 52 | */fastlane/Preview.html 53 | */fastlane/screenshots 54 | 55 | # Bundle artifacts 56 | *.jsbundle 57 | 58 | # CocoaPods 59 | /ios/Pods/ 60 | 61 | # Expo 62 | .expo/* 63 | web-build/ 64 | .vercel 65 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | arrowParens: 'always', 3 | bracketSpacing: true, 4 | jsxBracketSameLine: false, 5 | printWidth: 90, 6 | semi: false, 7 | singleQuote: true, 8 | tabWidth: 2, 9 | trailingComma: 'all', 10 | useTabs: false, 11 | } -------------------------------------------------------------------------------- /App.tsx: -------------------------------------------------------------------------------- 1 | import 'react-native-gesture-handler' 2 | import { createDrawerNavigator } from '@react-navigation/drawer' 3 | import { createStackNavigator } from '@react-navigation/stack' 4 | import { NavigationContainer } from '@react-navigation/native' 5 | import { Provider, useDispatch, useSelector } from 'react-redux' 6 | import { useColorScheme } from 'react-native' 7 | import * as Localization from 'expo-localization' 8 | import i18n from 'i18n-js' 9 | import React, { useEffect, useMemo, useState, useRef } from 'react' 10 | 11 | import { CabalProvider } from './lib' 12 | import { LocalizationContext } from './utils/Translations' 13 | import { RootState } from './app/rootReducer' 14 | import { setColorMode } from './features/themes/themesSlice' 15 | import AddCabalScreen from './screens/AddCabalScreen' 16 | import CabalSettingsScreen from './screens/CabalSettingsScreen' 17 | import ChannelBrowserScreen from './screens/ChannelBrowserScreen' 18 | import ChannelDetailScreen from './screens/ChannelDetailScreen' 19 | import ChannelScreen from './screens/ChannelScreen' 20 | import Sidebar from './components/Sidebar' 21 | import store from './app/store' 22 | import ThemeEditorScreen from './screens/ThemeEditorScreen' 23 | import useIsMobile from './hooks/useIsMobile' 24 | import UserProfileScreen from './screens/UserProfileScreen' 25 | 26 | const Stack = createStackNavigator() 27 | const Drawer = createDrawerNavigator() 28 | 29 | function ScreensContainer() { 30 | const colorMode = useColorScheme() 31 | const dispatch = useDispatch() 32 | const isMobile = useIsMobile() 33 | 34 | const { currentTheme } = useSelector((state: RootState) => state.themes) 35 | 36 | useEffect(() => { 37 | dispatch(setColorMode(colorMode)) 38 | }, [colorMode]) 39 | 40 | return ( 41 | 42 | } 44 | drawerStyle={isMobile ? { width: '90%' } : null} 45 | drawerType={isMobile ? 'front' : 'permanent'} 46 | initialRouteName="ChannelScreen" 47 | > 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | ) 58 | } 59 | 60 | export default function App() { 61 | const [locale, setLocale] = useState(Localization.locale) 62 | const localizationContext = useMemo( 63 | () => ({ 64 | t: (scope, options) => i18n.t(scope, { locale, ...options }), 65 | locale, 66 | setLocale, 67 | }), 68 | [locale], 69 | ) 70 | 71 | return ( 72 | 73 | 76 | 77 | 78 | 79 | 80 | 81 | ) 82 | } 83 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-cabal 2 | -------------------------------------------------------------------------------- /app/rootReducer.ts: -------------------------------------------------------------------------------- 1 | import { combineReducers } from '@reduxjs/toolkit' 2 | 3 | import cabalsSlice from '../features/cabals/cabalsSlice' 4 | import messagesSlice from '../features/cabals/messagesSlice' 5 | import themesSlice from '../features/themes/themesSlice' 6 | 7 | const rootReducer = combineReducers({ 8 | cabals: cabalsSlice, 9 | messages: messagesSlice, 10 | themes: themesSlice, 11 | }) 12 | 13 | export type RootState = ReturnType 14 | 15 | export default rootReducer 16 | -------------------------------------------------------------------------------- /app/store.ts: -------------------------------------------------------------------------------- 1 | import { configureStore, Action } from '@reduxjs/toolkit' 2 | import { ThunkAction } from 'redux-thunk' 3 | 4 | import rootReducer, { RootState } from './rootReducer' 5 | 6 | export type AppDispatch = typeof store.dispatch 7 | export type AppThunk = ThunkAction> 8 | 9 | const store = configureStore({ 10 | reducer: rootReducer 11 | }) 12 | 13 | if (process.env.NODE_ENV === 'development' && module.hot) { 14 | module.hot.accept('./rootReducer', () => { 15 | const newRootReducer = require('./rootReducer').default 16 | store.replaceReducer(newRootReducer) 17 | }) 18 | } 19 | 20 | export default store 21 | -------------------------------------------------------------------------------- /app/types.ts: -------------------------------------------------------------------------------- 1 | export interface AppProps { 2 | cabalKeys: string[] 3 | cabals: CabalProps[] 4 | cabalSettingsModalVisible: boolean 5 | channelBrowserModalVisible: boolean 6 | currentCabal?: CabalProps 7 | currentScreen: 'addCabal' | 'main' | 'loading' | 'settings' 8 | emojiPickerModalVisible: boolean 9 | selectedUser: UserProps 10 | sidebarLists: SidebarListsProps 11 | } 12 | 13 | export interface CabalProps { 14 | channels: ChannelProps[] 15 | channelsJoined: ChannelProps[] 16 | channelsFavorites: ChannelProps[] 17 | currentChannel: ChannelProps 18 | id: string 19 | key: string 20 | name?: string 21 | username: string 22 | users: UserProps[] 23 | } 24 | 25 | export interface CabalChannelProps { 26 | cabalKey: string 27 | channel?: ChannelProps 28 | } 29 | 30 | export interface ChannelProps { 31 | members: UserProps[] 32 | name: string 33 | topic: string 34 | } 35 | 36 | export interface LocalizationContextProps { 37 | locale: string 38 | setLocale: React.Dispatch> 39 | t: (scope: string, options?) => string 40 | } 41 | 42 | export interface MessageProps { 43 | content: string 44 | key: string 45 | timestamp: string 46 | user: UserProps 47 | } 48 | 49 | export interface UserProps { 50 | key: string 51 | name: string 52 | online: boolean 53 | } 54 | 55 | export type SidebarListsProps = SidebarListProps[] 56 | 57 | export interface SidebarListProps { 58 | id: string 59 | open: boolean 60 | title?: string 61 | } 62 | -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nikolaiwarner/react-cabal/55b02906ff5dc6d677f45458744bd03f7bcdca7e/assets/icon.png -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function (api) { 2 | api.cache(true) 3 | return { 4 | presets: ['babel-preset-expo'], 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /components/Avatar.tsx: -------------------------------------------------------------------------------- 1 | import Identicon from 'react-blockies' 2 | import React from 'react' 3 | import styled from 'styled-components/native' 4 | 5 | const Container = styled.View` 6 | align-items: center; 7 | background-color: #000; 8 | border-radius: ${(props) => props.size ?? '32px'}; 9 | display: flex; 10 | height: ${(props) => props.size ?? '32px'}; 11 | justify-content: center; 12 | width: ${(props) => props.size ?? '32px'}; 13 | ` 14 | 15 | const Name = styled.Text` 16 | color: #fff; 17 | text-transform: uppercase; 18 | font-size: 10px; 19 | font-weight: 700; 20 | ` 21 | 22 | interface AvatarProps { 23 | bgColor?: string 24 | name?: string 25 | onClick?: () => void 26 | size?: number 27 | } 28 | 29 | export default function Avatar(props: AvatarProps) { 30 | return ( 31 | 32 | {props.name?.substr(0, 2)} 33 | {/* */} 34 | 35 | ) 36 | } 37 | -------------------------------------------------------------------------------- /components/Button.tsx: -------------------------------------------------------------------------------- 1 | import { ViewStyle } from 'react-native' 2 | import { useTheme } from '@react-navigation/native' 3 | import React from 'react' 4 | import styled from 'styled-components/native' 5 | 6 | const Container = styled.TouchableOpacity` 7 | align-items: center; 8 | background-color: ${({ colors }) => colors.buttonBackground}; 9 | border-color: ${({ colors }) => colors.buttonBorder}; 10 | border-width: 1px; 11 | border-radius: 8px; 12 | justify-content: center; 13 | padding: 4px 8px; 14 | margin-top: 16px; 15 | ` 16 | 17 | const Title = styled.Text` 18 | color: ${({ colors }) => colors.buttonText}; 19 | font-size: 14px; 20 | text-transform: uppercase; 21 | ` 22 | 23 | interface ButtonProps { 24 | onPress: () => void 25 | style?: ViewStyle 26 | title: string 27 | } 28 | 29 | export default function Button(props: ButtonProps) { 30 | const { colors } = useTheme() 31 | 32 | return ( 33 | 34 | {props.title} 35 | 36 | ) 37 | } 38 | -------------------------------------------------------------------------------- /components/CabalList.tsx: -------------------------------------------------------------------------------- 1 | import { DrawerActions, useTheme } from '@react-navigation/native' 2 | import { Feather } from '@expo/vector-icons' 3 | import { Image, TouchableOpacity, Text, View } from 'react-native' 4 | import { useSelector, useDispatch } from 'react-redux' 5 | import React from 'react' 6 | import styled from 'styled-components/native' 7 | 8 | import { CabalProps } from '../app/types' 9 | import { focusCabal, showScreen } from '../features/cabals/cabalsSlice' 10 | import { RootState } from '../app/rootReducer' 11 | import { color } from 'react-native-reanimated' 12 | import { DrawerNavigationHelpers } from '@react-navigation/drawer/lib/typescript/src/types' 13 | 14 | const CabalListContainer = styled.View` 15 | /* flex-grow: 1; */ 16 | /* width: 32px; */ 17 | border-right-width: 1px; 18 | display: flex; 19 | flex-direction: column; 20 | height: 100%; 21 | overflow: hidden; 22 | ` 23 | 24 | const List = styled.View` 25 | padding-top: 16px; 26 | padding-bottom: 16px; 27 | padding-left: 16px; 28 | padding-right: 16px; 29 | overflow: scroll; 30 | ` 31 | 32 | const Item = styled.TouchableOpacity` 33 | /* cursor: pointer; */ 34 | /* transition: all 0.05s ease-in-out; */ 35 | align-items: center; 36 | border-radius: 48px; 37 | display: flex; 38 | font-size: 16px; 39 | height: 48px; 40 | justify-content: center; 41 | margin-bottom: 8px; 42 | margin-left: 0; 43 | margin-right: 0; 44 | margin-top: 0; 45 | opacity: 0.7; 46 | width: 48px; 47 | 48 | &:hover { 49 | opacity: 1; 50 | transform: scale(1.05); 51 | } 52 | 53 | &.active { 54 | border: none; 55 | } 56 | ` 57 | 58 | const ItemText = styled.Text` 59 | font-weight: 700; 60 | ` 61 | 62 | export default function CabalList(props: { navigation: DrawerNavigationHelpers }) { 63 | const { colors } = useTheme() 64 | const dispatch = useDispatch() 65 | 66 | const { cabals, currentCabal } = useSelector((state: RootState) => state.cabals) 67 | 68 | const onClickCabalListItem = (cabalKey) => { 69 | dispatch(focusCabal({ cabalKey })) 70 | props.navigation.dispatch(DrawerActions.toggleDrawer()) 71 | props.navigation.navigate('ChannelScreen') 72 | } 73 | 74 | const onClickAddCabalButton = () => { 75 | props.navigation.dispatch(DrawerActions.toggleDrawer()) 76 | props.navigation.navigate('AddCabalScreen') 77 | } 78 | 79 | const onClickAppSettingsButton = () => { 80 | props.navigation.dispatch(DrawerActions.toggleDrawer()) 81 | props.navigation.navigate('AppSettingsScreen') 82 | } 83 | 84 | return ( 85 | 86 | 87 | {cabals.length && 88 | cabals.map((cabal, index) => { 89 | const isCurrent = cabal.key === currentCabal.key 90 | return ( 91 | onClickCabalListItem(cabal.key)} 94 | style={{ 95 | backgroundColor: colors.card, 96 | }} 97 | > 98 | 99 | {cabal.key.substr(0, 2)} 100 | 101 | 102 | ) 103 | })} 104 | 105 | 106 | 107 | 108 | {/* 109 | 110 | */} 111 | 112 | ) 113 | } 114 | -------------------------------------------------------------------------------- /components/ChannelDetailPanel.tsx: -------------------------------------------------------------------------------- 1 | import { useNavigation, useTheme } from '@react-navigation/native' 2 | import { FontAwesome } from '@expo/vector-icons' 3 | import { ScrollView } from 'react-native' 4 | import { useSelector, useDispatch } from 'react-redux' 5 | import React, { useCallback, useContext } from 'react' 6 | import styled from 'styled-components/native' 7 | 8 | import { UserProps } from '../app/types' 9 | import { LocalizationContext } from '../utils/Translations' 10 | import Button from './Button' 11 | 12 | import PanelHeader from './PanelHeader' 13 | import PanelSection from './PanelSection' 14 | import SectionHeaderText from './SectionHeaderText' 15 | import { useChannel } from '../lib' 16 | 17 | export default function ChannelDetailPanel() { 18 | const { colors } = useTheme() 19 | const { t } = useContext(LocalizationContext) 20 | const { currentChannelMembers } = useChannel() 21 | 22 | const navigation = useNavigation() 23 | 24 | const renderPeerListItem = useCallback((user: UserProps) => { 25 | const onPressRow = () => { 26 | // TODO: remove redux dependency 27 | // dispatch(setSelectedUser(user)) 28 | // navigation.navigate('UserProfileScreen') 29 | } 30 | 31 | return ( 32 | 33 | 34 | {' '} 39 | {user.name || user.key.slice(0, 5)} 40 | 41 | 42 | ) 43 | }, []) 44 | 45 | const onPressClose = () => { 46 | navigation.navigate('ChannelScreen') 47 | } 48 | 49 | const onPressLeaveChannel = useCallback(() => {}, []) 50 | 51 | const onPressArchiveChannel = useCallback(() => {}, []) 52 | 53 | return ( 54 | 55 | 56 | 57 | 58 | 63 | 68 | 69 | 70 | 71 | {t('channel_members_list_header')} 72 | 73 | {currentChannelMembers.map(renderPeerListItem)} 74 | 75 | 76 | 77 | ) 78 | } 79 | 80 | const Container = styled.SafeAreaView`` 81 | 82 | const Row = styled.TouchableOpacity` 83 | /* cursor: pointer; */ 84 | padding-top: 8px; 85 | padding-bottom: 8px; 86 | padding-right: 16px; 87 | ` 88 | 89 | const RowText = styled.Text` 90 | /* cursor: pointer; */ 91 | font-size: 16px; 92 | ` 93 | -------------------------------------------------------------------------------- /components/ChannelHeader.tsx: -------------------------------------------------------------------------------- 1 | import { AntDesign, Feather } from '@expo/vector-icons' 2 | import { Text, TouchableOpacity } from 'react-native' 3 | import { useNavigation, useTheme } from '@react-navigation/native' 4 | import React, { useCallback, useContext } from 'react' 5 | import styled from 'styled-components/native' 6 | 7 | import { LocalizationContext } from '../utils/Translations' 8 | import MenuButton from '../components/MenuButton' 9 | import useIsMobile from '../hooks/useIsMobile' 10 | import { useChannel } from '../lib' 11 | 12 | export default function ChannelHeader() { 13 | const { colors } = useTheme() 14 | const { t } = useContext(LocalizationContext) 15 | 16 | const isMobile = useIsMobile() 17 | const navigation = useNavigation() 18 | 19 | const { currentChannel: currentChannelName, channels } = useChannel() 20 | 21 | const currentChannel = channels?.[currentChannelName] || {} 22 | 23 | const onPressFavorite = () => {} 24 | 25 | const onPressChannelDetails = useCallback(() => { 26 | navigation.navigate('ChannelDetailScreen') 27 | }, []) 28 | 29 | const onPressTopic = () => {} 30 | 31 | return ( 32 | 33 | 34 | {isMobile && } 35 | 36 | 37 | {currentChannel?.name}{' '} 38 | 39 | 40 | 41 | 42 | 43 | 44 | {currentChannel?.members?.size}{' '} 45 | ⋅{' '} 46 | 47 | 52 | {currentChannel.topic || t('channel_topic_placeholder')} 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | ) 64 | } 65 | 66 | const ChannelHeaderContainer = styled.View` 67 | align-items: center; 68 | border-bottom-width: 1px; 69 | display: flex; 70 | flex-direction: row; 71 | height: 62px; 72 | justify-content: space-between; 73 | padding-bottom: 8px; 74 | padding-top: 8px; 75 | padding: 16px; 76 | 77 | /* TODO: for electron: 78 | -webkit-app-region: drag; */ 79 | ` 80 | 81 | const MenuTitleContainer = styled.View` 82 | align-items: center; 83 | display: flex; 84 | flex-direction: row; 85 | flex-shrink: 1; 86 | ` 87 | 88 | const Title = styled.View`` 89 | 90 | const ChannelName = styled.Text` 91 | color: ${({ colors }) => colors.text}; 92 | font-size: 20px; 93 | font-weight: 900; 94 | margin-bottom: 4px; 95 | ` 96 | 97 | const ChannelInfo = styled.View` 98 | display: flex; 99 | flex-direction: row; 100 | ` 101 | 102 | const Topic = styled.Text` 103 | color: ${({ colors }) => colors.textSofter}; 104 | ` 105 | 106 | const Actions = styled.View`` 107 | -------------------------------------------------------------------------------- /components/ChannelListItem.tsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nikolaiwarner/react-cabal/55b02906ff5dc6d677f45458744bd03f7bcdca7e/components/ChannelListItem.tsx -------------------------------------------------------------------------------- /components/HelpText.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components/native' 3 | 4 | const HelpText = styled.Text` 5 | color: ${({ colors }) => colors.textSofter}; 6 | font-size: 14px; 7 | margin-top: 16px; 8 | ` 9 | 10 | export default HelpText 11 | -------------------------------------------------------------------------------- /components/Input.tsx: -------------------------------------------------------------------------------- 1 | import { useTheme } from '@react-navigation/native' 2 | import { ViewStyle } from 'react-native' 3 | import React from 'react' 4 | import styled from 'styled-components/native' 5 | 6 | const Container = styled.View` 7 | background-color: ${({ colors }) => colors.background}; 8 | border-color: ${({ colors }) => colors.border}; 9 | border-radius: 8px; 10 | border-width: 2px; 11 | display: flex; 12 | justify-content: center; 13 | margin-top: 8px; 14 | padding: 8px; 15 | ` 16 | 17 | const TextInput = styled.TextInput` 18 | border-width: 0; 19 | color: ${({ colors }) => colors.text}; 20 | font-size: 16px; 21 | ` 22 | 23 | interface InputProps { 24 | containerStyle?: ViewStyle 25 | defaultValue?: string 26 | onChangeText?: (text: string) => any 27 | placeholder?: string 28 | style?: ViewStyle 29 | value?: string 30 | } 31 | 32 | export default function Input(props: InputProps) { 33 | const { colors } = useTheme() 34 | 35 | return ( 36 | 37 | 45 | 46 | ) 47 | } 48 | -------------------------------------------------------------------------------- /components/MenuButton.tsx: -------------------------------------------------------------------------------- 1 | import { DrawerActions, useTheme, useNavigation } from '@react-navigation/native' 2 | import { Feather } from '@expo/vector-icons' 3 | import React from 'react' 4 | import styled from 'styled-components/native' 5 | 6 | const ButtonContainer = styled.TouchableOpacity` 7 | padding-right: 16px; 8 | ` 9 | 10 | export default function MenuButton() { 11 | const { colors } = useTheme() 12 | const navigation = useNavigation() 13 | 14 | return ( 15 | navigation.dispatch(DrawerActions.toggleDrawer())}> 16 | 17 | 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /components/Message.tsx: -------------------------------------------------------------------------------- 1 | import { TouchableOpacity } from 'react-native' 2 | import { useDispatch } from 'react-redux' 3 | import { useNavigation, useTheme } from '@react-navigation/native' 4 | import moment from 'moment' 5 | import React, { useCallback } from 'react' 6 | import styled from 'styled-components/native' 7 | 8 | import { MessageProps } from '../app/types' 9 | import { setSelectedUser } from '../features/cabals/cabalsSlice' 10 | import Avatar from './Avatar' 11 | 12 | interface MessageComponentProps { 13 | message: MessageProps 14 | repeatedName: boolean 15 | } 16 | 17 | const MessageContainer = styled.View` 18 | display: flex; 19 | flex-direction: row; 20 | margin-bottom: 16px; 21 | padding-top: 4px; 22 | /* width: 100%; */ 23 | ` 24 | 25 | const AvatarContainer = styled.TouchableOpacity` 26 | display: flex; 27 | padding-right: 16px; 28 | ` 29 | 30 | const Name = styled.Text` 31 | align-items: flex-end; 32 | color: ${({ colors }) => colors.text}; 33 | display: flex; 34 | font-size: 16px; 35 | font-weight: 700; 36 | margin-right: 8px; 37 | margin-top: -5px; 38 | ` 39 | 40 | const Timestamp = styled.Text` 41 | color: ${({ colors }) => colors.textSofter}; 42 | font-size: 12px; 43 | font-weight: 400; 44 | 45 | & .date--full { 46 | margin-left: 8px; 47 | opacity: 0; 48 | /* transition: opacity 0.2s ease-in-out; */ 49 | } 50 | ` 51 | 52 | const Content = styled.View` 53 | display: flex; 54 | flex-direction: column; 55 | line-height: 1.5; 56 | padding: 0 24px 0 0; 57 | 58 | &:hover .date .date--full { 59 | opacity: 1; 60 | } 61 | ` 62 | 63 | const StyledText = styled.Text` 64 | color: ${({ colors }) => colors.textSofter}; 65 | font-size: 16px; 66 | margin-left: ${(props) => (props.indent ? '32px;' : '0px')}; 67 | margin-right: 16px; 68 | margin-top: ${(props) => (props.indent ? '-12px;' : '0px')}; 69 | /* max-width: 800px; */ 70 | ` 71 | 72 | export default function Message(props: MessageComponentProps) { 73 | const { colors } = useTheme() 74 | const dispatch = useDispatch() 75 | const navigation = useNavigation() 76 | 77 | const renderDate = () => { 78 | const time = moment(props.message.timestamp) 79 | return ( 80 | 81 | ⋅ {time.format('h:mm A')} 82 | {/* {time.format('LL')} */} 83 | 84 | ) 85 | } 86 | 87 | const enrichText = (content) => { 88 | // TODO 89 | return content 90 | } 91 | 92 | const onPressUser = useCallback(() => { 93 | dispatch(setSelectedUser(props.message.user)) 94 | navigation.navigate('UserProfileScreen') 95 | }, []) 96 | 97 | return ( 98 | 99 | 100 | {props.repeatedName ? null : ( 101 | 102 | )} 103 | 104 | 105 | {props.repeatedName ? null : ( 106 | 107 | 108 | {props.message?.user?.name || 'conspirator'} {renderDate()} 109 | 110 | 111 | )} 112 | 113 | {enrichText(props.message.content)} 114 | 115 | 116 | 117 | ) 118 | } 119 | -------------------------------------------------------------------------------- /components/MessageComposer.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Button, 3 | Image, 4 | Platform, 5 | Text, 6 | TextInput, 7 | TouchableOpacity, 8 | View, 9 | } from 'react-native' 10 | import { Ionicons } from '@expo/vector-icons' 11 | import { useSelector, useDispatch } from 'react-redux' 12 | import { useTheme } from '@react-navigation/native' 13 | import React, { useRef } from 'react' 14 | import styled from 'styled-components/native' 15 | 16 | // import '../../node_modules/emoji-mart/css/emoji-mart.css' 17 | // import { Picker } from 'emoji-mart' 18 | 19 | import { publishMessage } from '../features/cabals/messagesSlice' 20 | import { RootState } from '../app/rootReducer' 21 | import { setEmojiPickerModalVisible } from '../features/cabals/cabalsSlice' 22 | import { ChannelProps } from '../app/types' 23 | import { useMessage } from '../lib' 24 | 25 | const MessageComposerContainer = styled.View` 26 | border-top-color: ${(props) => props.colors.border}; 27 | border-top-width: 1px; 28 | padding: 8px 16px 8px 16px; 29 | ` 30 | 31 | const InputWrapper = styled.View` 32 | align-items: center; 33 | border-radius: 3px; 34 | border: 2px solid rgba(0, 0, 0, 0.25); 35 | display: flex; 36 | justify-content: space-between; 37 | padding: 8px 0; 38 | 39 | &:hover { 40 | border-color: rgba(0, 0, 0, 0.5); 41 | } 42 | ` 43 | 44 | const Input = styled.View` 45 | align-content: center; 46 | display: flex; 47 | flex-direction: row; 48 | padding: 0 8px 0 0; 49 | width: 100%; 50 | ` 51 | 52 | const Textarea = styled.TextInput` 53 | border: 0; 54 | flex-grow: 1; 55 | font-size: 16px; 56 | max-height: 200px; 57 | padding: 0 16px 58 | ${Platform.OS === 'web' && 59 | ` 60 | outlineWidth: 0; 61 | resize: none; 62 | `}; 63 | ` 64 | 65 | const EmojiPickerContainer = styled.View` 66 | /* position: 'absolute'; 67 | bottom: '100px'; 68 | right: '16px'; 69 | display: 'none'; */ 70 | ` 71 | 72 | const ToggleEmojiPickerButton = styled.View`` 73 | 74 | export default function MessageComposer() { 75 | const { colors } = useTheme() 76 | const dispatch = useDispatch() 77 | 78 | const { cabals, currentCabal } = useSelector((state: RootState) => state.cabals) 79 | const { sendMessage } = useMessage() 80 | const formFieldRef = useRef(null) 81 | const textInputRef = useRef(null) 82 | 83 | const onKeyPress = (event) => { 84 | if (event.key === 'Enter') { 85 | onSubmit() 86 | } else if (event.key === 'Escape') { 87 | textInputRef.current.blur() 88 | } else if (event.key === 'ArrowUp') { 89 | console.log('UP') 90 | } else if (event.key === 'ArrowDown') { 91 | console.log('DOWN') 92 | } 93 | } 94 | 95 | const onSubmit = (_event?) => { 96 | sendMessage(textInputRef.current.value) 97 | textInputRef.current.value = '' 98 | } 99 | 100 | const addEmoji = () => {} 101 | 102 | const focusInput = () => {} 103 | 104 | const toggleEmojis = () => { 105 | // dispatch(setEmojiPickerModalVisible(!cabals.emojiPickerModalVisible)) 106 | } 107 | 108 | return ( 109 | 110 | 111 | 112 | 119 | 120 | 121 | 122 | 123 | {/* */} 124 | {/* addEmoji(e)} 126 | native 127 | sheetSize={64} 128 | // showPreview={false} 129 | autoFocus 130 | emoji='point_up' 131 | title='Pick an emoji...' 132 | /> */} 133 | {/* */} 134 | 135 | {/* */} 136 | 137 | 138 | 139 | ) 140 | } 141 | -------------------------------------------------------------------------------- /components/MessageContent.tsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nikolaiwarner/react-cabal/55b02906ff5dc6d677f45458744bd03f7bcdca7e/components/MessageContent.tsx -------------------------------------------------------------------------------- /components/MessageList.tsx: -------------------------------------------------------------------------------- 1 | import { useTheme } from '@react-navigation/native' 2 | import moment from 'moment' 3 | import React, { useCallback, useEffect, useState } from 'react' 4 | import styled from 'styled-components/native' 5 | import { MessageProps } from '../app/types' 6 | import Message from './Message' 7 | import { useMessage } from '../lib' 8 | 9 | interface SectionProps { 10 | title: string 11 | } 12 | 13 | export default function MessageList() { 14 | const { colors } = useTheme() 15 | 16 | const [sectionData, setSectionData] = useState([]) 17 | const { messages } = useMessage() 18 | 19 | useEffect(() => { 20 | const dataByDate = {} 21 | for (const { value, sender } of messages) { 22 | const date = moment(value.timestamp).format('YYYY-MM-DD') 23 | if (!dataByDate[date]) dataByDate[date] = [] 24 | dataByDate[date].push({ 25 | content: value?.content?.text, 26 | key: sender, 27 | timestamp: value?.timestamp, 28 | user: { name: sender, key: sender, online: true }, 29 | }) 30 | } 31 | 32 | const data = Object.entries(dataByDate).map(([title, data]) => ({ 33 | title, 34 | data: data?.reverse(), 35 | })) 36 | 37 | setSectionData(data) 38 | }, [messages]) 39 | 40 | const renderItem = useCallback(({ item }: { item: MessageProps }) => { 41 | // TODO: fix styling of consecutive messages by the same author 42 | const repeatedName = false // message.user.key === lastMessageUserKey 43 | // lastMessageUserKey = message.user.key 44 | return 45 | }, []) 46 | 47 | const renderSectionHeader = useCallback(({ section }: { section: SectionProps }) => { 48 | return ( 49 | 50 | 51 | 52 | {moment(section.title).format('LL')} 53 | 54 | 55 | 56 | ) 57 | }, []) 58 | 59 | // if its an empty channel with no messages, show a placeholder 60 | if (messages.length === 0) { 61 | return ( 62 | 63 | {/* This is a new channel. Send a message to start things off */} 64 | 65 | ) 66 | } 67 | 68 | return ( 69 | index} 72 | renderItem={renderItem} 73 | renderSectionHeader={renderSectionHeader} 74 | sections={sectionData} 75 | /> 76 | ) 77 | } 78 | 79 | const MessageSectionList = styled.SectionList` 80 | overflow: scroll; 81 | padding-bottom: 16px; 82 | padding-left: 16px; 83 | padding-right: 16px; 84 | padding-top: 16px; 85 | ` 86 | 87 | const SectionHeader = styled.View` 88 | align-items: center; 89 | 90 | border-top-color: ${({ colors }) => colors.border}; 91 | border-top-width: 1px; 92 | margin-top: 8px; 93 | padding-bottom: 8px; 94 | ` 95 | 96 | const SectionHeaderTextContainer = styled.View` 97 | background-color: ${({ colors }) => colors.background}; 98 | border-radius: 8px; 99 | margin-top: -14px; 100 | padding-bottom: 4px; 101 | padding-left: 16px; 102 | padding-right: 16px; 103 | padding-top: 4px; 104 | ` 105 | 106 | const SectionHeaderText = styled.Text` 107 | color: ${({ colors }) => colors.textSofter}; 108 | font-size: 16px; 109 | ` 110 | 111 | const StarterMessage = styled.View` 112 | flex-grow: 1; 113 | margin: 1rem; 114 | overflow: scroll; 115 | ` 116 | -------------------------------------------------------------------------------- /components/PanelHeader.tsx: -------------------------------------------------------------------------------- 1 | import { useTheme } from '@react-navigation/native' 2 | import { Feather } from '@expo/vector-icons' 3 | import React, { ReactElement } from 'react' 4 | import styled from 'styled-components/native' 5 | import SectionHeaderText from './SectionHeaderText' 6 | 7 | const Container = styled.View` 8 | align-items: center; 9 | border-bottom-color: ${({ colors }) => colors.border}; 10 | border-bottom-width: 1px; 11 | display: flex; 12 | flex-direction: row; 13 | height: 62px; 14 | justify-content: space-between; 15 | padding: 16px 0 16px 16px; 16 | ` 17 | 18 | const Actions = styled.View` 19 | align-items: center; 20 | display: flex; 21 | flex-direction: row; 22 | justify-content: space-between; 23 | ` 24 | 25 | const CloseButton = styled.TouchableOpacity` 26 | padding: 0 16px; 27 | ` 28 | 29 | interface PanelHeaderProps { 30 | onPressClose?: () => void 31 | renderActions?: () => ReactElement 32 | title: string 33 | } 34 | 35 | export default function PanelHeader(props: PanelHeaderProps) { 36 | const { colors } = useTheme() 37 | 38 | return ( 39 | 40 | {props.title} 41 | 42 | {props.renderActions && props.renderActions()} 43 | {props.onPressClose && ( 44 | 45 | 46 | 47 | )} 48 | 49 | 50 | ) 51 | } 52 | -------------------------------------------------------------------------------- /components/PanelSection.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components/native' 3 | 4 | const PanelSection = styled.View` 5 | border-bottom-width: 1px; 6 | border-bottom-color: ${({ colors }) => colors.border}; 7 | padding: 16px; 8 | ` 9 | 10 | export default PanelSection 11 | -------------------------------------------------------------------------------- /components/SectionHeaderText.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components/native' 3 | 4 | const SectionHeaderText = styled.Text` 5 | color: ${({ colors }) => colors.text}; 6 | font-size: 18px; 7 | font-weight: 700; 8 | ` 9 | 10 | export default SectionHeaderText 11 | -------------------------------------------------------------------------------- /components/Sidebar.tsx: -------------------------------------------------------------------------------- 1 | import { DrawerActions, useTheme } from '@react-navigation/native' 2 | import { 3 | DrawerContentComponentProps, 4 | DrawerContentOptions, 5 | } from '@react-navigation/drawer' 6 | import { Feather, FontAwesome } from '@expo/vector-icons' 7 | import { ScrollView, Text, TouchableOpacity, View } from 'react-native' 8 | import { useSelector, useDispatch } from 'react-redux' 9 | import React, { useCallback, useContext } from 'react' 10 | import styled from 'styled-components/native' 11 | 12 | import { ChannelProps, UserProps } from '../app/types' 13 | import { setSelectedUser } from '../features/cabals/cabalsSlice' 14 | import { LocalizationContext } from '../utils/Translations' 15 | import { RootState } from '../app/rootReducer' 16 | import CabalList from './CabalList' 17 | import SidebarHeader from './SidebarHeader' 18 | import SidebarList from './SidebarList' 19 | import { useChannel } from '../lib' 20 | import { useUsers } from '../lib/hooks/useUsers' 21 | 22 | const SidebarContainer = styled.SafeAreaView` 23 | display: flex; 24 | flex-direction: row; 25 | overflow: scroll; 26 | height: 100%; 27 | ` 28 | 29 | const Row = styled.TouchableOpacity` 30 | /* cursor: pointer; */ 31 | padding-top: 8px; 32 | padding-bottom: 8px; 33 | padding-left: 16px; 34 | padding-right: 16px; 35 | ` 36 | 37 | const RowText = styled.Text` 38 | /* cursor: pointer; */ 39 | font-size: 16px; 40 | ` 41 | 42 | export default function Sidebar( 43 | props: DrawerContentComponentProps, 44 | ) { 45 | const { colors } = useTheme() 46 | const { t } = useContext(LocalizationContext) 47 | const dispatch = useDispatch() 48 | 49 | const { currentCabal, sidebarLists } = useSelector((state: RootState) => state.cabals) 50 | 51 | const { joinedChannels, currentChannel, focusChannel, channels } = useChannel() 52 | 53 | const { users = [] } = useUsers() 54 | 55 | const userList = Object.values(users) 56 | 57 | const onPressOpenChannelBrowser = useCallback(() => { 58 | props.navigation.dispatch(DrawerActions.toggleDrawer()) 59 | props.navigation.navigate('ChannelBrowserScreen') 60 | }, []) 61 | 62 | const renderChannelListHeaderActionButton = useCallback(() => { 63 | return ( 64 | 68 | 69 | 70 | ) 71 | }, []) 72 | 73 | const renderChannelListItem = (channel: ChannelProps, isActive?: boolean) => { 74 | const color = currentChannel === channel.name ? colors.text : colors.textSofter 75 | return ( 76 | focusChannel(channel.name)}> 77 | 78 | 79 | 80 | {channel.name} 81 | 82 | 83 | ) 84 | } 85 | 86 | const renderPeerListItem = useCallback((user: UserProps) => { 87 | const onPressRow = () => { 88 | dispatch(setSelectedUser(user)) 89 | props.navigation.dispatch(DrawerActions.toggleDrawer()) 90 | props.navigation.navigate('UserProfileScreen') 91 | } 92 | 93 | return ( 94 | 95 | 96 | 101 | {' '} 102 | {user.name || user.key.slice(0, 5)} 103 | 104 | 105 | ) 106 | }, []) 107 | 108 | return ( 109 | 110 | 111 | {currentCabal && ( 112 | 113 | 114 | {sidebarLists.map((sidebarList) => { 115 | if (sidebarList.id === 'favorites') { 116 | return ( 117 | 125 | ) 126 | } else if (sidebarList.id === 'channels_joined') { 127 | return ( 128 | ({ name: item }))} // TODO: fix this 131 | key={sidebarList.id} 132 | renderHeaderActionButton={renderChannelListHeaderActionButton} 133 | renderItem={renderChannelListItem} 134 | sidebarList={sidebarList} 135 | title={t('sidebarlist_channels')} 136 | /> 137 | ) 138 | } else if (sidebarList.id === 'peers') { 139 | return ( 140 | 147 | ) 148 | } else { 149 | // TODO: Custom lists 150 | } 151 | })} 152 | 153 | )} 154 | 155 | ) 156 | } 157 | -------------------------------------------------------------------------------- /components/SidebarHeader.tsx: -------------------------------------------------------------------------------- 1 | import { DrawerActions, useTheme } from '@react-navigation/native' 2 | import { 3 | DrawerContentScrollView, 4 | DrawerItemList, 5 | DrawerItem, 6 | } from '@react-navigation/drawer' 7 | import { DrawerNavigationHelpers } from '@react-navigation/drawer/lib/typescript/src/types' 8 | import { Feather } from '@expo/vector-icons' 9 | import { Text, TouchableOpacity, View } from 'react-native' 10 | import { useSelector, useDispatch } from 'react-redux' 11 | import React, { useCallback } from 'react' 12 | import styled from 'styled-components/native' 13 | 14 | import { CabalProps } from '../app/types' 15 | import { RootState } from '../app/rootReducer' 16 | import Avatar from './Avatar' 17 | 18 | const Container = styled.View` 19 | align-items: center; 20 | display: flex; 21 | flex-direction: row; 22 | height: 61px; 23 | justify-content: space-between; 24 | padding-top: 0; 25 | width: 236px; 26 | padding-right: 8px; 27 | ` 28 | 29 | const AvatarNameContainer = styled.TouchableOpacity` 30 | padding-left: 16px; 31 | display: flex; 32 | flex-direction: row; 33 | ` 34 | 35 | const CabalName = styled.Text` 36 | font-size: 16px; 37 | font-weight: 700; 38 | overflow: hidden; 39 | padding-left: 8px; 40 | padding-right: 8px; 41 | ` 42 | 43 | const UserName = styled.Text` 44 | font-size: 14px; 45 | overflow: hidden; 46 | padding-left: 8px; 47 | padding-right: 8px; 48 | ` 49 | 50 | export default function SidebarHeader(props: { navigation: DrawerNavigationHelpers }) { 51 | const { colors } = useTheme() 52 | 53 | const { currentCabal } = useSelector((state: RootState) => state.cabals) 54 | 55 | const onPressCabalSettings = useCallback(() => { 56 | props.navigation.dispatch(DrawerActions.toggleDrawer()) 57 | props.navigation.navigate('CabalSettingsScreen') 58 | }, []) 59 | 60 | const onPressUserName = useCallback(() => { 61 | // TODO: rename user 62 | }, []) 63 | 64 | return ( 65 | 66 | 67 | 68 | 69 | 70 | {currentCabal.name ?? currentCabal.key} 71 | 72 | 73 | {/* {currentCabal.name ?? currentCabal.key} */} 74 | nickwarner 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | ) 83 | } 84 | -------------------------------------------------------------------------------- /components/SidebarList.tsx: -------------------------------------------------------------------------------- 1 | import { DrawerActions, useTheme, useNavigation } from '@react-navigation/native' 2 | import { Octicons } from '@expo/vector-icons' 3 | import { Text, TouchableOpacity, View } from 'react-native' 4 | import { useDispatch, useSelector } from 'react-redux' 5 | import React, { ReactElement, useCallback } from 'react' 6 | import styled from 'styled-components/native' 7 | import { updateSidebarList } from '../features/cabals/cabalsSlice' 8 | import { SidebarListProps, SidebarListsProps } from '../app/types' 9 | 10 | const SidebarListContainer = styled.View` 11 | border-color: ${(props) => props.colors.border}; 12 | border-top-width: 1px; 13 | padding-bottom: 12px; 14 | ` 15 | 16 | const ListHeader = styled.View` 17 | align-items: center; 18 | display: flex; 19 | flex-direction: row; 20 | justify-content: space-between; 21 | padding-bottom: 4px; 22 | padding-left: 16px; 23 | padding-top: 12px; 24 | ` 25 | 26 | const TitleContainer = styled.TouchableOpacity` 27 | align-items: center; 28 | display: flex; 29 | flex-direction: row; 30 | justify-content: space-between; 31 | ` 32 | 33 | const Title = styled.Text` 34 | color: ${(props) => props.colors.textSofter}; 35 | text-transform: uppercase; 36 | ` 37 | 38 | const ListBody = styled.View`` 39 | 40 | interface SidebarListComponentProps { 41 | activeItem?: any 42 | items?: any[] 43 | onClickRow?: (item: any) => void 44 | onToggleClosed?: () => void 45 | renderHeaderActionButton?: () => ReactElement 46 | renderItem: (item: any, isActive?: boolean) => ReactElement 47 | sidebarList: SidebarListProps 48 | title: string 49 | } 50 | 51 | export default function SidebarList(props: SidebarListComponentProps) { 52 | const { colors } = useTheme() 53 | const dispatch = useDispatch() 54 | 55 | const onPressToggleSidebarList = () => { 56 | dispatch( 57 | updateSidebarList({ 58 | ...props.sidebarList, 59 | open: !props.sidebarList.open, 60 | }), 61 | ) 62 | } 63 | 64 | return ( 65 | 66 | 67 | 68 | 69 | 74 | {' '} 75 | {props.title} 76 | 77 | 78 | {props.renderHeaderActionButton && props.renderHeaderActionButton()} 79 | 80 | {props.sidebarList.open && ( 81 | 82 | {props.items?.length && 83 | props.items?.map((item) => props.renderItem(item, item === props.activeItem))} 84 | 85 | )} 86 | 87 | ) 88 | } 89 | -------------------------------------------------------------------------------- /components/UserProfilePanel.tsx: -------------------------------------------------------------------------------- 1 | import { ScrollView, Text, View } from 'react-native' 2 | import { useNavigation, useTheme } from '@react-navigation/native' 3 | import { useSelector, useDispatch } from 'react-redux' 4 | import React, { useCallback } from 'react' 5 | import styled from 'styled-components/native' 6 | 7 | import { RootState } from '../app/rootReducer' 8 | import Avatar from './Avatar' 9 | import Button from './Button' 10 | import HelpText from './HelpText' 11 | import PanelHeader from './PanelHeader' 12 | import PanelSection from './PanelSection' 13 | import SectionHeaderText from './SectionHeaderText' 14 | import useIsMobile from '../hooks/useIsMobile' 15 | 16 | const Container = styled.SafeAreaView`` 17 | 18 | const Name = styled.Text` 19 | color: ${({ colors }) => colors.text}; 20 | font-size: 18px; 21 | font-weight: 700; 22 | margin-top: 16px; 23 | ` 24 | 25 | const UserKey = styled.Text` 26 | color: ${({ colors }) => colors.textSofter}; 27 | margin-top: 16px; 28 | ` 29 | 30 | export default function UserProfilePanel() { 31 | const { colors } = useTheme() 32 | const dispatch = useDispatch() 33 | const isMobile = useIsMobile() 34 | const navigation = useNavigation() 35 | 36 | const { currentCabal, selectedUser: user } = useSelector( 37 | (state: RootState) => state.cabals, 38 | ) 39 | 40 | const onPressClose = useCallback(() => { 41 | navigation.navigate('ChannelScreen') 42 | }, []) 43 | 44 | const onPressToggleHidePeer = useCallback(() => {}, []) 45 | 46 | const onPressToggleModPeer = useCallback(() => {}, []) 47 | 48 | const onPressToggleAdminPeer = useCallback(() => {}, []) 49 | 50 | return ( 51 | 52 | 53 | 54 | 55 | 56 | {user?.name} 57 | {user?.key} 58 | 59 | 60 | Moderation 61 | 66 | 67 | Hiding a peer hides all of their past and future messages in all channels.{' '} 68 | 69 | 74 | 75 | Adding another user as a moderator for you will apply their moderation 76 | settings to how you see this cabal. 77 | 78 | 83 | 84 | Adding another user as an admin for you will apply their moderation settings 85 | to how you see this cabal. 86 | 87 | 88 | 89 | 90 | ) 91 | } 92 | -------------------------------------------------------------------------------- /definitions.d.ts: -------------------------------------------------------------------------------- 1 | import '@react-navigation/native' 2 | 3 | declare module '@react-navigation/native' { 4 | export type ExtendedTheme = { 5 | dark: boolean 6 | colors: { 7 | // Default 8 | primary: string 9 | background: string 10 | card: string 11 | text: string 12 | border: string 13 | notification: string 14 | 15 | //Custom 16 | buttonBackground: string 17 | buttonBorder: string 18 | buttonText: string 19 | textHighlight: string 20 | textSofter: string 21 | } 22 | name: string 23 | } 24 | export function useTheme(): ExtendedTheme 25 | } 26 | -------------------------------------------------------------------------------- /features/cabals/cabalsSlice.ts: -------------------------------------------------------------------------------- 1 | import { createSlice, PayloadAction } from '@reduxjs/toolkit' 2 | 3 | import moment from 'moment' 4 | // import remark from 'remark' 5 | // import remarkEmoji from 'remark-emoji' 6 | // import remarkReact from 'remark-react' 7 | // import throttle from 'lodash.throttle' 8 | 9 | import { AppThunk } from '../../app/store' 10 | import { listCommands, processCommand } from '../../utils/textInputCommands' 11 | import { generateUniqueName } from '../../utils/helpers' 12 | // import { 13 | // focusChannel as sendFocusChannel, 14 | // joinChannel as sendJoinChannel, 15 | // leaveChannel as sendLeaveChannel, 16 | // removeCabal as sendRemoveCabal, 17 | // setChannelTopic as sendSetChannelTopic, 18 | // setUsername as sendSetUsername, 19 | // focusCabal as sendfocusCabal, 20 | // } from '../../utils/cabal-render-ipc' 21 | import { 22 | AppProps, 23 | CabalChannelProps, 24 | ChannelProps, 25 | SidebarListProps, 26 | UserProps, 27 | } from '../../app/types' 28 | 29 | import { defaultCabal, defaultCabals } from '../../utils/fakeData' 30 | 31 | interface TextInputCommandProps { 32 | cabalKey: string 33 | text: string 34 | } 35 | 36 | const initialState: AppProps = { 37 | cabalKeys: ['1eef9ad64e284691b7c6f6310e39204b5f92765e36102046caaa6a7ff8c02d74'], 38 | cabals: defaultCabals, 39 | cabalSettingsModalVisible: false, 40 | channelBrowserModalVisible: false, 41 | currentCabal: defaultCabal, 42 | currentScreen: 'loading', 43 | emojiPickerModalVisible: false, 44 | selectedUser: null, 45 | sidebarLists: [ 46 | { id: 'favorites', open: true }, 47 | { id: 'channels_joined', open: true }, 48 | { id: 'peers', open: true }, 49 | ], 50 | } 51 | 52 | const cabalsSlice = createSlice({ 53 | name: 'cabals', 54 | initialState, 55 | reducers: { 56 | addCabal(state, action) { 57 | state.cabals.push(action.payload) 58 | }, 59 | addCabalKey(state, action) { 60 | state.cabalKeys.push(action.payload) 61 | }, 62 | hideAllModals(state) { 63 | state.emojiPickerModalVisible = false 64 | state.channelBrowserModalVisible = false 65 | state.cabalSettingsModalVisible = false 66 | }, 67 | setEmojiPickerModalVisible(state, action: PayloadAction) { 68 | state.emojiPickerModalVisible = action.payload 69 | }, 70 | setCabalSettingsModalVisible(state, action: PayloadAction) { 71 | state.cabalSettingsModalVisible = action.payload 72 | }, 73 | setChannelBrowserModalVisible(state, action: PayloadAction) { 74 | state.channelBrowserModalVisible = action.payload 75 | }, 76 | setCurrentCabal(state, action: PayloadAction) { 77 | const cabal = state.cabals.find((cabal) => cabal.key === action.payload.cabalKey) 78 | if (cabal) { 79 | state.currentCabal = cabal 80 | } 81 | }, 82 | setCurrentChannel(state, action: PayloadAction) { 83 | console.log('setCurrentChannel', action) 84 | state.currentCabal.currentChannel = action.payload.channel 85 | }, 86 | setCurrentScreen(state, action: PayloadAction) { 87 | state.currentScreen = action.payload 88 | }, 89 | updateCabal(state, action) { 90 | const updatedCabal = action.payload 91 | const cabals = state.cabals.map((cabal) => { 92 | if (cabal.key === updatedCabal.key) { 93 | return updatedCabal 94 | } else { 95 | return cabal 96 | } 97 | }) 98 | state.cabals = cabals 99 | }, 100 | setSelectedUser(state, action: PayloadAction) { 101 | state.selectedUser = action.payload 102 | }, 103 | updateSidebarList(state, action: PayloadAction) { 104 | state.sidebarLists = state.sidebarLists.map((panel) => { 105 | return panel.id === action.payload.id ? action.payload : panel 106 | }) 107 | }, 108 | }, 109 | }) 110 | 111 | export const { 112 | addCabal, 113 | addCabalKey, 114 | hideAllModals, 115 | setCabalSettingsModalVisible, 116 | setChannelBrowserModalVisible, 117 | setCurrentCabal, 118 | setCurrentChannel, 119 | setCurrentScreen, 120 | setEmojiPickerModalVisible, 121 | setSelectedUser, 122 | updateSidebarList, 123 | } = cabalsSlice.actions 124 | 125 | export default cabalsSlice.reducer 126 | 127 | export const focusCabal = (props: CabalChannelProps): AppThunk => async (dispatch) => { 128 | // sendfocusCabal(props) 129 | 130 | dispatch(setCurrentCabal({ cabalKey: props.cabalKey })) 131 | if (props.channel) { 132 | dispatch(focusChannel(props)) 133 | } 134 | dispatch(hideAllModals()) 135 | } 136 | 137 | export const focusChannel = (props: CabalChannelProps): AppThunk => async (dispatch) => { 138 | // sendFocusChannel(props) 139 | dispatch(setCurrentChannel(props)) 140 | } 141 | 142 | export const joinChannel = (props: CabalChannelProps): AppThunk => async (dispatch) => { 143 | // sendJoinChannel(props) 144 | dispatch(setCurrentChannel(props)) 145 | } 146 | 147 | export const leaveChannel = (props: CabalChannelProps): AppThunk => async (dispatch) => { 148 | // sendLeaveChannel(props) 149 | } 150 | 151 | export const listTextInputCommands = (): AppThunk => async (dispatch) => { 152 | // dispatch(listCommands()) 153 | } 154 | 155 | export const onTextInputCommand = (props: TextInputCommandProps): AppThunk => async ( 156 | dispatch, 157 | ) => { 158 | // dispatch(processCommand(props)) 159 | } 160 | 161 | export const removeCabal = ({ cabalKey }: { cabalKey: string }): AppThunk => async ( 162 | dispatch, 163 | ) => { 164 | // sendRemoveCabal({ cabalKey }) 165 | } 166 | 167 | export const saveCabalSettings = (): AppThunk => async (dispatch) => {} 168 | 169 | export const setChannelTopic = ({ 170 | cabalKey, 171 | channel, 172 | topic, 173 | }: { 174 | cabalKey: string 175 | channel: string 176 | topic: string 177 | }): AppThunk => async (dispatch) => { 178 | // sendSetChannelTopic({ cabalKey, channel, topic }) 179 | } 180 | 181 | export const setUsername = ({ 182 | cabalKey, 183 | username, 184 | }: { 185 | cabalKey: string 186 | username: string 187 | }): AppThunk => async (dispatch) => { 188 | // sendSetUsername({ cabalKey, username }) 189 | } 190 | 191 | export const showChannelBrowserModal = (): AppThunk => async (dispatch) => { 192 | dispatch(setChannelBrowserModalVisible(true)) 193 | } 194 | 195 | export const showScreen = (screenName: string): AppThunk => async (dispatch) => { 196 | dispatch(hideAllModals()) 197 | dispatch(setCurrentScreen(screenName)) 198 | } 199 | -------------------------------------------------------------------------------- /features/cabals/messagesSlice.ts: -------------------------------------------------------------------------------- 1 | import { createSlice, PayloadAction } from '@reduxjs/toolkit' 2 | import moment from 'moment' 3 | // import remark from 'remark' 4 | // import remarkEmoji from 'remark-emoji' 5 | // import remarkReact from 'remark-react' 6 | // import throttle from 'lodash.throttle' 7 | 8 | import { AppThunk } from '../../app/store' 9 | import { generateUniqueName } from '../../utils/helpers' 10 | import { listCommands, processCommand } from '../../utils/textInputCommands' 11 | import { MessageProps } from '../../app/types' 12 | 13 | import { defaultMessages, manyMessages } from '../../utils/fakeData' 14 | 15 | // import { 16 | // publishMessage as sendPublishMessage, 17 | // publishStatusMessage as sendPublishStatusMessage, 18 | // } from '../../utils/cabal-render-ipc' 19 | 20 | interface MessagesProps { 21 | messages: MessageProps[] 22 | } 23 | 24 | interface TextInputCommandProps { 25 | cabalKey: string 26 | text: string 27 | } 28 | 29 | let initialState: MessagesProps = { 30 | messages: manyMessages, 31 | } 32 | 33 | const cabalsSlice = createSlice({ 34 | name: 'cabals', 35 | initialState, 36 | reducers: { 37 | addMessage(state, action) { 38 | console.log('addMessageaddMessage', action.payload) 39 | state.messages.push(action.payload) 40 | }, 41 | }, 42 | }) 43 | 44 | export const { addMessage } = cabalsSlice.actions 45 | 46 | export default cabalsSlice.reducer 47 | 48 | export const publishMessage = ({ 49 | cabalKey, 50 | channel, 51 | message, 52 | }: { 53 | cabalKey: string 54 | channel: string 55 | message: string 56 | }): AppThunk => async (dispatch) => { 57 | // sendPublishMessage({ cabalKey, channel, message }) 58 | } 59 | 60 | export const publishStatusMessage = ({ 61 | cabalKey, 62 | channel, 63 | text, 64 | }: { 65 | cabalKey: string 66 | channel: string 67 | text: string 68 | }): AppThunk => async (dispatch) => { 69 | // sendPublishStatusMessage({ cabalKey, channel, text }) 70 | } 71 | 72 | export const listTextInputCommands = (): AppThunk => async (dispatch) => { 73 | // dispatch(listCommands()) 74 | } 75 | 76 | export const onTextInputCommand = (props: TextInputCommandProps): AppThunk => async ( 77 | dispatch, 78 | ) => { 79 | // dispatch(processCommand(props)) 80 | } 81 | -------------------------------------------------------------------------------- /features/themes/themesSlice.ts: -------------------------------------------------------------------------------- 1 | import { AppThunk } from '../../app/store' 2 | import { ColorSchemeName } from 'react-native' 3 | import { createSlice, PayloadAction } from '@reduxjs/toolkit' 4 | import { ExtendedTheme } from '@react-navigation/native' 5 | 6 | import { CabalDarkTheme, CabalLightTheme } from '../../utils/Themes' 7 | 8 | interface ThemesProps { 9 | currentTheme: ExtendedTheme 10 | isCustomTheme: boolean 11 | } 12 | 13 | export const DEFAULT_DARK_THEMES = [CabalDarkTheme] 14 | export const DEFAULT_LIGHT_THEMES = [CabalLightTheme] 15 | export const DEFAULT_THEMES = [...DEFAULT_DARK_THEMES, ...DEFAULT_LIGHT_THEMES] 16 | 17 | const initialState: ThemesProps = { 18 | currentTheme: DEFAULT_THEMES[0], 19 | isCustomTheme: false, 20 | } 21 | 22 | const themesSlice = createSlice({ 23 | name: 'themes', 24 | initialState, 25 | reducers: { 26 | setTheme(state, action: PayloadAction) { 27 | state.isCustomTheme = !DEFAULT_THEMES.map((theme) => theme.name).includes( 28 | action.payload.name, 29 | ) 30 | state.currentTheme = action.payload 31 | }, 32 | }, 33 | }) 34 | 35 | export const { setTheme } = themesSlice.actions 36 | 37 | export default themesSlice.reducer 38 | 39 | // On change of system color mode: dark or light 40 | export const setColorMode = (colorSchemeName: ColorSchemeName): AppThunk => async ( 41 | dispatch, 42 | getState, 43 | ) => { 44 | if (!getState().themes.isCustomTheme) { 45 | const theme = 46 | colorSchemeName === 'dark' ? DEFAULT_DARK_THEMES[0] : DEFAULT_LIGHT_THEMES[0] 47 | dispatch(setTheme(theme)) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /hooks/useCabal.ts: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react' 2 | import { Platform } from 'react-native' 3 | import { generateUniqueName } from '../utils/helpers' 4 | import { Provider, useDispatch, useSelector } from 'react-redux' 5 | import { addMessage } from '../features/cabals/messagesSlice' 6 | 7 | interface UseCabalProps {} 8 | 9 | //@ts-ignore 10 | const cabalClient = Platform.OS === 'web' ? new window.CabalClient() : undefined 11 | 12 | export default function useCabal(props?: UseCabalProps) { 13 | const dispatch = useDispatch() 14 | 15 | const initializeCabal = ({ key, nickname }: { key: string; nickname?: string }) => { 16 | nickname = nickname ?? generateUniqueName() 17 | 18 | if (!cabalClient) { 19 | console.error('cabal client is not available') 20 | return 21 | } 22 | 23 | console.log({ cabalClient }) 24 | 25 | cabalClient.addCabal(key).then((cabalDetails) => { 26 | // cabalDetails.getLocalKey((err, localkey) => { 27 | // console.log('local key:', localkey) 28 | // }) 29 | 30 | cabalDetails.on('new-message', ({ channel, author, message }) => { 31 | console.log('Got a message: ', author, channel, message) 32 | 33 | dispatch( 34 | addMessage({ 35 | channel, 36 | content: message.value.content.text, 37 | key: message.key, 38 | timestamp: message.value.timestamp, 39 | user: { 40 | key: author.key, 41 | local: author.local, 42 | name: author.name, 43 | online: author.online, 44 | }, 45 | }), 46 | ) 47 | }) 48 | 49 | cabalDetails.on('update', (payload) => { 50 | console.log('Recieved: ', payload) 51 | }) 52 | 53 | cabalDetails.on('peer-added', (k) => { 54 | console.log('new peer', k) 55 | cabalDetails.publishMessage({ 56 | type: 'chat/text', 57 | content: { 58 | text: 'peer-added: ' + k.key, 59 | channel: 'default', 60 | }, 61 | }) 62 | }) 63 | 64 | cabalDetails.publishNick(nickname, () => { 65 | cabalDetails.publishMessage({ 66 | type: 'chat/text', 67 | content: { 68 | text: 'Hey there! Im ' + nickname, 69 | channel: 'default', 70 | }, 71 | }) 72 | }) 73 | }) 74 | } 75 | 76 | return { cabalClient, initializeCabal } 77 | } 78 | -------------------------------------------------------------------------------- /hooks/useIsMobile.tsx: -------------------------------------------------------------------------------- 1 | import { useWindowDimensions } from 'react-native' 2 | import React from 'react' 3 | 4 | export default function useIsMobile() { 5 | const windowWidth = useWindowDimensions().width 6 | 7 | return windowWidth < 800 8 | } 9 | -------------------------------------------------------------------------------- /lib/CabalProvider.tsx: -------------------------------------------------------------------------------- 1 | import Client from '../web/cabal-client-bundle' 2 | import React, { useEffect, useState } from 'react' 3 | 4 | export function createCabalClient(dbdir = `/tmp/.cabal-desktop/v2`) { 5 | const client = new Client({ 6 | config: { 7 | dbdir, 8 | }, 9 | }) 10 | 11 | return client 12 | } 13 | 14 | export const CabalContext = React.createContext(null) 15 | 16 | export function CabalProvider({ children, initCabal, dbdir }: any) { 17 | const [client, setClient] = useState() 18 | 19 | async function initializeCabals() { 20 | const cabalClient = createCabalClient(dbdir) 21 | if (initCabal) { 22 | try { 23 | await cabalClient.addCabal(initCabal) 24 | setClient(cabalClient) 25 | } catch (e) { 26 | console.log('error is', e) 27 | } 28 | } 29 | } 30 | useEffect(() => { 31 | initializeCabals() 32 | }, []) 33 | 34 | return {children} 35 | } 36 | -------------------------------------------------------------------------------- /lib/hooks/useCabal.ts: -------------------------------------------------------------------------------- 1 | import { useContext, useState, useEffect } from 'react'; 2 | import { CabalContext } from '../CabalProvider'; 3 | 4 | export function useCabal() { 5 | const [cabals, setCabals] = useState>([]); 6 | const [currentCabal, setCurrentCabal] = useState(); 7 | 8 | const client = useContext(CabalContext); 9 | 10 | useEffect(() => { 11 | if (!client) return; 12 | const cabals = client.getCabalKeys(); 13 | const cabal = client.getCurrentCabal(); 14 | setCabals(cabals); 15 | setCurrentCabal(cabal); 16 | 17 | cabals.forEach((cabal) => { 18 | const selectedCabal = client.getDetails(cabal); 19 | selectedCabal.on('cabal-focus', (event: any) => { 20 | setCurrentCabal(client.getCurrentCabal()); 21 | }); 22 | }); 23 | }, [client]); 24 | 25 | // add a new cabal 26 | function addCabal(key: string) { 27 | client.addCabal(key).then(() => { 28 | const cabals = client.getCabalKeys(); 29 | setCabals(cabals); 30 | }); 31 | } 32 | 33 | function focusCabal(key) { 34 | client?.focusCabal(key); 35 | } 36 | 37 | return { 38 | cabals, 39 | currentCabal, 40 | addCabal, 41 | focusCabal, 42 | }; 43 | } 44 | -------------------------------------------------------------------------------- /lib/hooks/useChannel.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, useRef } from 'react' 2 | import { useCabal } from './useCabal' 3 | import { useUsers } from './useUsers' 4 | 5 | export function useChannel() { 6 | const [channels, setChannels] = useState([]) // all channels 7 | const [joinedChannels, setJoinedChannels] = useState([]) // all channels joined by the user 8 | const [currentChannel, setCurrentChannel] = useState('default') // current selected channel 9 | const [currentChannelMembers, setCurrentChannelMembers] = useState([]) // members of the current channel 10 | const { users } = useUsers() 11 | 12 | const { currentCabal } = useCabal() 13 | 14 | const focusChannel = (channel: string) => { 15 | if (joinedChannels?.includes(channel) && channel !== currentChannel) { 16 | currentCabal.focusChannel(channel) 17 | } 18 | } 19 | 20 | const joinChannel = (channel: string) => { 21 | if (!joinedChannels?.includes(channel)) { 22 | currentCabal.joinChannel(channel) 23 | } 24 | } 25 | 26 | // set topic for current channel 27 | const setCurrentChannelTopic = (topic: string) => { 28 | currentCabal.setChannelTopic(currentChannel, topic) 29 | } 30 | 31 | useEffect(() => { 32 | // update member list! 33 | const memberSet = channels?.[currentChannel]?.members?.values() 34 | if (memberSet) { 35 | const memberList = Array.from(memberSet) 36 | .map((member) => users?.[member]) 37 | .filter((i) => !!i) 38 | setCurrentChannelMembers(memberList) 39 | } 40 | }, [users, currentChannel]) 41 | 42 | useEffect(() => { 43 | if (!currentCabal) return 44 | 45 | const channel = currentCabal.getCurrentChannel() 46 | 47 | setChannels(currentCabal.channels) 48 | setCurrentChannel(channel) 49 | setJoinedChannels(currentCabal.getJoinedChannels()) 50 | // setMembers(channelMembers); 51 | 52 | if (currentCabal) { 53 | currentCabal.on('channel-focus', ({ channel }: { channel: string }) => { 54 | setCurrentChannel(channel) 55 | }) 56 | 57 | currentCabal.on('channel-join', (channel) => { 58 | // TODO: actions any if on joining channels 59 | }) 60 | 61 | if (channel === '!status') { 62 | focusChannel('default') 63 | } 64 | } 65 | }, [currentCabal]) 66 | 67 | return { 68 | channels, 69 | joinedChannels, 70 | currentChannel, 71 | focusChannel, 72 | joinChannel, 73 | currentChannelMembers, 74 | setCurrentChannelTopic, 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /lib/hooks/useMessage.ts: -------------------------------------------------------------------------------- 1 | import { useContext, useState, useEffect } from 'react' 2 | import { CabalContext } from '../CabalProvider' 3 | import { useCabal } from './useCabal' 4 | import { useChannel } from './useChannel' 5 | import { useUsers } from './useUsers' 6 | 7 | export function useMessage() { 8 | const [messages, setMessages] = useState>([]) 9 | const client = useContext(CabalContext) 10 | const { currentChannel } = useChannel() 11 | const { currentCabal } = useCabal() 12 | 13 | const { users } = useUsers() 14 | 15 | const messageHandler = (msg: any) => { 16 | const { message, channel: messageChannel } = msg 17 | 18 | if (messageChannel === currentChannel) { 19 | const currentMessage = { 20 | ...msg.message, 21 | sender: users?.[message.key]?.name || message?.key?.slice(0, 5), 22 | } 23 | 24 | setMessages((messages) => [...messages, currentMessage]) 25 | } 26 | } 27 | useEffect(() => { 28 | if (!client) return 29 | client.getMessages( 30 | { 31 | currentChannel, 32 | }, 33 | (allMessages: Array) => { 34 | const messageList = allMessages.map((msg: any) => { 35 | return { 36 | ...msg, 37 | sender: users?.[msg.key]?.name || msg.key.slice(0, 5), 38 | } 39 | }) 40 | setMessages(messageList) 41 | }, 42 | ) 43 | const cabal = client.getCurrentCabal() 44 | 45 | cabal.on('new-message', messageHandler) 46 | return () => cabal.removeListener('new-message', messageHandler) 47 | }, [currentChannel, currentCabal, client, users]) 48 | 49 | function sendMessage(message) { 50 | currentCabal.publishMessage({ 51 | type: 'chat/text', 52 | content: { 53 | text: message, 54 | channel: currentChannel, 55 | }, 56 | }) 57 | } 58 | 59 | return { 60 | messages, 61 | sendMessage, 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /lib/hooks/useUsers.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, useContext } from 'react' 2 | import { useCabal } from './useCabal' 3 | 4 | export function useUsers() { 5 | const { currentCabal } = useCabal() 6 | const [users, setUsers] = useState() 7 | 8 | useEffect(() => { 9 | if (currentCabal) { 10 | const userList = currentCabal.getUsers() 11 | setUsers(userList) 12 | 13 | currentCabal.on('started-peering', (key, name) => { 14 | setUsers(currentCabal.getUsers()) 15 | }) 16 | 17 | currentCabal.on('stopped-peering', (key, name) => { 18 | setUsers(currentCabal.getUsers()) 19 | }) 20 | } 21 | }, [currentCabal]) 22 | 23 | return { 24 | users, 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /lib/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './CabalProvider'; 2 | export * from './hooks/useCabal'; 3 | export * from './hooks/useChannel'; 4 | export * from './hooks/useMessage'; 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "@react-native-community/masked-view": "0.1.11", 4 | "@react-native-community/picker": "^1.8.1", 5 | "@react-navigation/drawer": "^6.1.4", 6 | "@react-navigation/native": "^6.0.2", 7 | "@react-navigation/stack": "^6.0.7", 8 | "@reduxjs/toolkit": "^1.6.1", 9 | "expo": "^43.0.0", 10 | "expo-localization": "~11.0.0", 11 | "hex-color-regex": "^1.1.0", 12 | "i18n-js": "^3.8.0", 13 | "moment": "^2.29.1", 14 | "prop-types": "^15.7.2", 15 | "react": "17.0.2", 16 | "react-blockies": "^1.4.1", 17 | "react-dom": "17.0.2", 18 | "react-native": "https://github.com/expo/react-native/archive/sdk-43.tar.gz", 19 | "react-native-gesture-handler": "~1.10.2", 20 | "react-native-qrcode-svg": "^6.1.1", 21 | "react-native-reanimated": "~2.2.0", 22 | "react-native-safe-area-context": "3.3.2", 23 | "react-native-screens": "~3.8.0", 24 | "react-native-svg": "12.1.1", 25 | "react-native-web": "0.17.5", 26 | "react-redux": "^7.2.5", 27 | "styled-components": "^5.3.1" 28 | }, 29 | "devDependencies": { 30 | "@babel/core": "^7.12.9", 31 | "@types/react": "~17.0.21", 32 | "@types/react-native": "~0.64.12", 33 | "typescript": "~4.3.5" 34 | }, 35 | "scripts": { 36 | "start": "expo start", 37 | "android": "expo start --android", 38 | "ios": "expo start --ios", 39 | "web": "expo start --web", 40 | "eject": "expo eject", 41 | "deploy-web": "expo build:web && vercel web-build" 42 | }, 43 | "private": true 44 | } 45 | -------------------------------------------------------------------------------- /screens/AddCabalScreen.tsx: -------------------------------------------------------------------------------- 1 | import { SafeAreaView, ScrollView, TextInput } from 'react-native' 2 | import { useTheme } from '@react-navigation/native' 3 | import { useSelector } from 'react-redux' 4 | import React, { useCallback, useContext, useState } from 'react' 5 | 6 | import { LocalizationContext } from '../utils/Translations' 7 | import { RootState } from '../app/rootReducer' 8 | import Button from '../components/Button' 9 | import Input from '../components/Input' 10 | import PanelHeader from '../components/PanelHeader' 11 | import PanelSection from '../components/PanelSection' 12 | import SectionHeaderText from '../components/SectionHeaderText' 13 | 14 | function AddCabalScreen({ navigation }) { 15 | const { colors } = useTheme() 16 | const { t } = useContext(LocalizationContext) 17 | 18 | const { cabals } = useSelector((state: RootState) => state.cabals) 19 | 20 | const [newCabalKeyInput, setNewCabalKeyInput] = useState() 21 | const [newCabalUsernameInput, setNewCabalUsernameInput] = useState() 22 | 23 | const onPressClose = useCallback(() => { 24 | navigation.navigate('ChannelScreen') 25 | }, []) 26 | 27 | const onPressCreate = useCallback(() => {}, []) 28 | 29 | const onPressJoin = useCallback(() => { 30 | console.log({ newCabalKeyInput, newCabalUsernameInput }) 31 | }, [newCabalKeyInput, newCabalUsernameInput]) 32 | 33 | return ( 34 | 35 | {!!cabals.length && ( 36 | 37 | )} 38 | 39 | 40 | 41 | {t('add_cabal_join_cabal_section_title')} 42 | 43 | 47 | 51 | 52 | 53 | 54 | 55 | 56 | {t('add_cabal_new_cabal_section_title')} 57 | 58 | 59 | 60 | 61 | 62 | ) 63 | } 64 | 65 | export default AddCabalScreen 66 | -------------------------------------------------------------------------------- /screens/AppSettingsScreen.tsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nikolaiwarner/react-cabal/55b02906ff5dc6d677f45458744bd03f7bcdca7e/screens/AppSettingsScreen.tsx -------------------------------------------------------------------------------- /screens/CabalSettingsScreen.tsx: -------------------------------------------------------------------------------- 1 | import { NavigationContainer, DrawerActions, useTheme } from '@react-navigation/native' 2 | import { TextInput, Text, View, SafeAreaView, ScrollView, Switch } from 'react-native' 3 | import { useSelector, useDispatch } from 'react-redux' 4 | import QRCode from 'react-native-qrcode-svg' 5 | import React, { useCallback, useContext } from 'react' 6 | 7 | import { LocalizationContext } from '../utils/Translations' 8 | import { RootState } from '../app/rootReducer' 9 | import Button from '../components/Button' 10 | import HelpText from '../components/HelpText' 11 | import PanelHeader from '../components/PanelHeader' 12 | import PanelSection from '../components/PanelSection' 13 | import SectionHeaderText from '../components/SectionHeaderText' 14 | 15 | const qrLogo = require('../assets/icon.png') 16 | 17 | function CabalSettingsScreen({ navigation }) { 18 | const { colors } = useTheme() 19 | const { t } = useContext(LocalizationContext) 20 | 21 | const { cabals, currentCabal } = useSelector((state: RootState) => state.cabals) 22 | 23 | const onPressClose = useCallback(() => { 24 | navigation.navigate('ChannelScreen') 25 | }, []) 26 | 27 | const onPressCopyCabalKey = useCallback(() => {}, [currentCabal.key]) 28 | 29 | const onPressEditTheme = useCallback(() => { 30 | navigation.navigate('ThemeEditorScreen') 31 | }, []) 32 | 33 | const onPressRemoveCabal = useCallback(() => {}, []) 34 | 35 | const shareableCabalKey = useCallback(() => { 36 | // TODO 37 | const adminParams = '' 38 | const modParams = '' 39 | return `cabal://${currentCabal.key}?${adminParams}&${modParams}` 40 | }, [currentCabal.key]) 41 | 42 | return ( 43 | 44 | 45 | 46 | 47 | 48 | {t('cabal_settings_invite_header')} 49 | 50 | {t('cabal_settings_invite_body')} 51 | 52 | 56 | 57 | 58 | 59 | 60 | 61 | {t('cabal_settings_cabal_name_header')} 62 | 63 | {t('cabal_settings_cabal_name_body')} 64 | 65 | 66 | 67 | 68 | 69 | {t('cabal_settings_notifications_header')} 70 | 71 | {t('cabal_settings_notifications_body')} 72 | 73 | 74 | 75 | 76 | 77 | {t('cabal_settings_edit_theme_header')} 78 | 79 | 83 | 84 | 85 | 86 | 87 | {t('cabal_settings_remove_cabal_header')} 88 | 89 | {t('cabal_settings_remove_cabal_body')} 90 | 96 | 97 | 98 | 99 | ) 100 | } 101 | 102 | export default CabalSettingsScreen 103 | -------------------------------------------------------------------------------- /screens/ChannelBrowserScreen.tsx: -------------------------------------------------------------------------------- 1 | import { Feather } from '@expo/vector-icons' 2 | import { NavigationContainer, DrawerActions, useTheme } from '@react-navigation/native' 3 | import { TextInput, Text, View, SafeAreaView } from 'react-native' 4 | import { useSelector, useDispatch } from 'react-redux' 5 | import React, { useCallback, useContext } from 'react' 6 | import styled from 'styled-components/native' 7 | 8 | import { LocalizationContext } from '../utils/Translations' 9 | import { RootState } from '../app/rootReducer' 10 | import Button from '../components/Button' 11 | import PanelHeader from '../components/PanelHeader' 12 | import SectionHeaderText from '../components/SectionHeaderText' 13 | import PanelSection from '../components/PanelSection' 14 | import { ChannelProps } from '../app/types' 15 | import { ScrollView } from 'react-native-gesture-handler' 16 | import { useChannel } from '../lib' 17 | 18 | function ChannelBrowserScreen({ navigation }) { 19 | const { colors } = useTheme() 20 | const { t } = useContext(LocalizationContext) 21 | const { joinedChannels, channels = {}, joinChannel } = useChannel() 22 | 23 | const { currentCabal } = useSelector((state: RootState) => state.cabals) 24 | 25 | const onPressClose = useCallback(() => { 26 | navigation.navigate('ChannelScreen') 27 | }, []) 28 | 29 | const onPressCreateChannel = useCallback(() => {}, []) 30 | 31 | const onPressJoinChannel = (channel) => { 32 | // TODO: add channel joining logic 33 | joinChannel(channel) 34 | navigation.navigate('ChannelScreen') 35 | } 36 | 37 | const renderPanelHeaderActions = useCallback(() => { 38 | return ( 39 | 43 | ) 44 | }, []) 45 | 46 | const renderChannelRow = (channel: ChannelProps) => { 47 | return ( 48 | onPressJoinChannel(channel?.name)}> 49 | {channel.name} 50 | 51 | {channel?.members?.size}{' '} 52 | 53 | 54 | {channel.topic} 55 | 56 | ) 57 | } 58 | 59 | const joinableChannels = Object.values(channels)?.filter( 60 | (channel: { name: string }) => !joinedChannels.includes(channel.name), 61 | ) 62 | const joinedChannelsDetails = Object.values(channels)?.filter( 63 | (channel: { name: string }) => joinedChannels.includes(channel.name), 64 | ) 65 | 66 | return ( 67 | 68 | 73 | 74 | {!!joinableChannels.length && ( 75 | 76 | 77 | {t('channel_browser_joinable_channels_list_title')} 78 | 79 | {joinableChannels.map(renderChannelRow)} 80 | 81 | )} 82 | {!!currentCabal.channelsJoined.length && ( 83 | 84 | 85 | {t('channel_browser_joined_channels_list_title')} 86 | 87 | {joinedChannelsDetails.map(renderChannelRow)} 88 | 89 | )} 90 | 91 | 92 | ) 93 | } 94 | 95 | export default ChannelBrowserScreen 96 | 97 | const Row = styled.TouchableOpacity` 98 | padding: 16px 16px 16px 0; 99 | ` 100 | 101 | const Name = styled.Text` 102 | color: ${({ colors }) => colors.text}; 103 | font-size: 18px; 104 | font-weight: 700; 105 | margin-bottom: 4px; 106 | ` 107 | 108 | const MemberCount = styled.Text` 109 | color: ${({ colors }) => colors.textHighlight}; 110 | ` 111 | 112 | const Topic = styled.Text` 113 | color: ${({ colors }) => colors.textSofter}; 114 | ` 115 | -------------------------------------------------------------------------------- /screens/ChannelDetailScreen.tsx: -------------------------------------------------------------------------------- 1 | import { SafeAreaView } from 'react-native' 2 | import * as React from 'react' 3 | 4 | import ChannelDetailPanel from '../components/ChannelDetailPanel' 5 | 6 | function ChannelDetailScreen() { 7 | return ( 8 | 9 | 10 | 11 | ) 12 | } 13 | 14 | export default ChannelDetailScreen 15 | -------------------------------------------------------------------------------- /screens/ChannelScreen.tsx: -------------------------------------------------------------------------------- 1 | import { SafeAreaView, View } from 'react-native' 2 | import { useTheme } from '@react-navigation/native' 3 | import React from 'react' 4 | import styled from 'styled-components/native' 5 | 6 | import ChannelDetailPanel from '../components/ChannelDetailPanel' 7 | import ChannelHeader from '../components/ChannelHeader' 8 | import MessageComposer from '../components/MessageComposer' 9 | import MessageList from '../components/MessageList' 10 | import useIsMobile from '../hooks/useIsMobile' 11 | 12 | const Container = styled.SafeAreaView` 13 | display: flex; 14 | flex-direction: row; 15 | ` 16 | 17 | const ChannelContainerWrapper = styled.View` 18 | display: flex; 19 | flex-grow: 1; 20 | height: 100vh; 21 | ` 22 | 23 | const ChannelContainer = styled.View` 24 | display: flex; 25 | flex-grow: 1; 26 | flex-direction: column; 27 | height: 100%; 28 | ` 29 | 30 | const Panel = styled.View` 31 | border-left-color: ${({ colors }) => colors.border}; 32 | border-left-width: 1px; 33 | /* height: 100%; */ 34 | width: 300px; 35 | ` 36 | 37 | export default function ChannelScreen({ navigation }) { 38 | const { colors } = useTheme() 39 | const isMobile = useIsMobile() 40 | 41 | if (isMobile) { 42 | return ( 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | ) 51 | } else { 52 | return ( 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | ) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /screens/HomeScreen.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import App from './HomeScreen' 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div') 7 | ReactDOM.render(, div) 8 | ReactDOM.unmountComponentAtNode(div) 9 | }) 10 | -------------------------------------------------------------------------------- /screens/HomeScreen.tsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nikolaiwarner/react-cabal/55b02906ff5dc6d677f45458744bd03f7bcdca7e/screens/HomeScreen.tsx -------------------------------------------------------------------------------- /screens/ThemeEditorScreen.tsx: -------------------------------------------------------------------------------- 1 | import { Picker } from '@react-native-community/picker' 2 | import { SafeAreaView, ScrollView } from 'react-native' 3 | import { useNavigation, useTheme } from '@react-navigation/native' 4 | import { useSelector, useDispatch } from 'react-redux' 5 | import hexColorRegex from 'hex-color-regex' 6 | import React, { useCallback, useContext, useEffect } from 'react' 7 | import styled from 'styled-components/native' 8 | 9 | import { DEFAULT_THEMES, setTheme } from '../features/themes/themesSlice' 10 | import { LocalizationContext } from '../utils/Translations' 11 | import { RootState } from '../app/rootReducer' 12 | import PanelHeader from '../components/PanelHeader' 13 | import PanelSection from '../components/PanelSection' 14 | import SectionHeaderText from '../components/SectionHeaderText' 15 | import Input from '../components/Input' 16 | 17 | const Row = styled.View` 18 | align-items: center; 19 | display: flex; 20 | flex-direction: row; 21 | justify-content: space-between; 22 | ` 23 | 24 | const Name = styled.Text`` 25 | 26 | function ChannelDetailScreen() { 27 | const { colors } = useTheme() 28 | const { t } = useContext(LocalizationContext) 29 | const dispatch = useDispatch() 30 | const navigation = useNavigation() 31 | 32 | const { currentTheme } = useSelector((state: RootState) => state.themes) 33 | 34 | const onPressClose = useCallback(() => { 35 | navigation.navigate('ChannelScreen') 36 | }, []) 37 | 38 | const onChangeCustomColorValue = useCallback( 39 | ({ key, value }) => { 40 | if (hexColorRegex().test(value)) { 41 | dispatch( 42 | setTheme({ 43 | ...currentTheme, 44 | name: 'Custom', 45 | colors: { 46 | ...currentTheme.colors, 47 | [key]: value, 48 | }, 49 | }), 50 | ) 51 | } 52 | }, 53 | [currentTheme], 54 | ) 55 | 56 | const keys = Object.keys(currentTheme.colors) 57 | 58 | const defaultCustomTheme = { 59 | ...currentTheme, 60 | name: 'Custom', 61 | } 62 | 63 | return ( 64 | 65 | 66 | 67 | 68 | Pick a Theme 69 | { 72 | const newTheme = DEFAULT_THEMES[index] 73 | if (newTheme) { 74 | dispatch(setTheme(newTheme)) 75 | } else { 76 | dispatch(setTheme(defaultCustomTheme)) 77 | } 78 | }} 79 | > 80 | {[...DEFAULT_THEMES, defaultCustomTheme].map((theme, index) => { 81 | return 82 | })} 83 | 84 | 85 | 86 | {currentTheme.name === 'Custom' && ( 87 | 88 | Custom Theme 89 | {keys.map((key) => { 90 | return ( 91 | 92 | {key} 93 | onChangeCustomColorValue({ key, value })} 97 | /> 98 | 99 | ) 100 | })} 101 | 102 | )} 103 | 104 | 105 | ) 106 | } 107 | 108 | export default ChannelDetailScreen 109 | -------------------------------------------------------------------------------- /screens/UserProfileScreen.tsx: -------------------------------------------------------------------------------- 1 | import { SafeAreaView } from 'react-native' 2 | import * as React from 'react' 3 | 4 | import UserProfilePanel from '../components/UserProfilePanel' 5 | 6 | function UserProfileScreen() { 7 | return ( 8 | 9 | 10 | 11 | ) 12 | } 13 | 14 | export default UserProfileScreen 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "jsx": "react-native", 4 | "target": "esnext", 5 | "lib": [ 6 | "dom", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "noEmit": true, 12 | "allowSyntheticDefaultImports": true, 13 | "resolveJsonModule": true, 14 | "esModuleInterop": true, 15 | "moduleResolution": "node" 16 | }, 17 | "extends": "expo/tsconfig.base" 18 | } 19 | -------------------------------------------------------------------------------- /utils/Themes.ts: -------------------------------------------------------------------------------- 1 | import { DarkTheme, DefaultTheme, ExtendedTheme } from '@react-navigation/native' 2 | 3 | /* 4 | Default color strings from React Navigation: 5 | - primary (string): The primary color of the app used to tint various elements. Usually you'll want to use your brand color for this. 6 | - background (string): The color of various backgrounds, such as background color for the screens. 7 | - card (string): The background color of card-like elements, such as headers, tab bars etc. 8 | - text (string): The text color of various elements. 9 | - border (string): The color of borders, e.g. header border, tab bar border etc. 10 | - notification (string): The color of Tab Navigator badge. 11 | 12 | Colors strings unique to Cabal. See `definitions.d.ts` to extend: 13 | - textHighlight (string): Text color that pops a bit more than `text` 14 | - textSofter (string): Text color that's softer than `text` 15 | */ 16 | 17 | export const CabalLightTheme: ExtendedTheme = { 18 | dark: false, 19 | colors: { 20 | ...DefaultTheme.colors, 21 | background: '#fff', 22 | buttonBackground: '#fff', 23 | buttonBorder: 'rgb(28, 28, 33)', 24 | buttonText: 'rgb(28, 28, 33)', 25 | primary: 'rgb(90, 72, 236)', 26 | text: 'rgb(28, 28, 33)', 27 | textHighlight: 'rgb(168, 153, 223)', 28 | textSofter: 'rgb(32, 32, 32)', 29 | }, 30 | name: 'Cabal Light', 31 | } 32 | 33 | export const CabalDarkTheme: ExtendedTheme = { 34 | dark: true, 35 | colors: { 36 | ...DarkTheme.colors, 37 | background: 'rgb(28, 28, 33)', 38 | buttonBackground: 'rgb(28, 28, 33)', 39 | buttonBorder: '#fff', 40 | buttonText: '#fff', 41 | border: 'rgb(70, 70, 70)', 42 | primary: 'rgb(90, 72, 236)', 43 | text: '#fff', 44 | textHighlight: 'rgb(168, 153, 223)', 45 | textSofter: 'rgb(169, 169, 169)', 46 | }, 47 | name: 'Cabal Dark', 48 | } 49 | -------------------------------------------------------------------------------- /utils/Translations.ts: -------------------------------------------------------------------------------- 1 | import { LocalizationContextProps } from '../app/types' 2 | import i18n from 'i18n-js' 3 | import React, { createContext } from 'react' 4 | 5 | export const LocalizationContext = createContext>({}) 6 | 7 | const Translations = { 8 | en: { 9 | add_cabal_join_button: 'Join', 10 | add_cabal_join_cabal_section_title: 'Join a Cabal', 11 | add_cabal_key_input_placeholder: 'cabal://', 12 | add_cabal_name_input_placeholder: 'Pick a nickname (optional)', 13 | add_cabal_new_cabal_button: 'Create', 14 | add_cabal_new_cabal_section_title: 'Create a New Cabal', 15 | add_cabal_title: 'Add a Cabal', 16 | archive_channel_button: 'Archive Channel', 17 | cabal_settings_copy_key_button: 'Copy Key', 18 | cabal_settings_invite_header: 'Invite People', 19 | cabal_settings_invite_body: 'Share this key with others to let them join the cabal.', 20 | cabal_settings_cabal_name_body: 21 | 'Set a local name for this cabal. Only you can see this.', 22 | cabal_settings_cabal_name_header: 'Cabal Name', 23 | cabal_settings_edit_theme_button: 'Theme Editor', 24 | cabal_settings_edit_theme_header: 'Theme', 25 | cabal_settings_notifications_body: 26 | 'Display a notification for new messages for this cabal when a channel is in the background.', 27 | cabal_settings_notifications_header: 'Notifications', 28 | cabal_settings_remove_cabal_body: 29 | 'Press the button below to locally remove this cabal from this client. Messages may still exist on peer clients.', 30 | cabal_settings_remove_cabal_button: 'Remove Cabal ({{key}}...)', 31 | cabal_settings_remove_cabal_header: 'Remove this cabal', 32 | cabal_settings_title: 'Cabal Settings', 33 | channel_browser_archived_channels_list_title: 'Archived Channels', 34 | channel_browser_create_channel_button: 'Create a New Channel', 35 | channel_browser_joinable_channels_list_title: 'Channels you can join', 36 | channel_browser_joined_channels_list_title: 'Channels you belong to', 37 | channel_browser_title: 'Browse Channels', 38 | channel_members_list_header: 'Channel Members', 39 | channel_panel_header: 'Channel Details', 40 | channel_topic_placeholder: 'Click to add a topic', 41 | leave_channel_button: 'Leave Channel', 42 | sidebarlist_channels: 'Channels', 43 | sidebarlist_favorites: 'Favorites', 44 | sidebarlist_peers: 'Peers', 45 | theme_editor_screen_title: 'Theme Editor', 46 | }, 47 | es: { 48 | add_cabal_join_button: 'Entrar', 49 | add_cabal_join_cabal_section_title: 'Únete a un Cabal', 50 | add_cabal_key_input_placeholder: 'cabal://', 51 | add_cabal_name_input_placeholder: 'Elija un apodo (opcional)', 52 | add_cabal_new_cabal_button: 'Crear', 53 | add_cabal_new_cabal_section_title: 'Crea una nueva Cabal', 54 | add_cabal_title: 'Agregar un Cabal', 55 | archive_channel_button: 'Archivar este canal', 56 | cabal_settings_cabal_name_header: 'Nombre', 57 | cabal_settings_cabal_name_body: 58 | 'Establezca un nombre local para esto. Solo tú puedes ver esto.', 59 | cabal_settings_copy_key_button: 'Copiar clave', 60 | cabal_settings_edit_theme_button: 'Theme Editor', 61 | cabal_settings_edit_theme_header: 'Theme', 62 | cabal_settings_invite_body: 63 | 'Comparta esta clave con otras personas para que puedan unirse.', 64 | cabal_settings_invite_header: 'Invitar a la gente', 65 | cabal_settings_notifications_body: 66 | 'Muestra una notificación para nuevos mensajes para esto cuando un canal está en segundo plano.', 67 | cabal_settings_notifications_header: 'Notificaciones', 68 | cabal_settings_remove_cabal_body: 69 | 'Presione el botón de abajo para eliminarlo localmente de este cliente. Es posible que todavía existan mensajes en clientes del mismo nivel.', 70 | cabal_settings_remove_cabal_header: 'Eliminar', 71 | cabal_settings_remove_cabal_button: 'Eliminar ({{key}}...)', 72 | cabal_settings_title: 'Ajustes', 73 | channel_browser_archived_channels_list_title: 'Canales archivados', 74 | channel_browser_create_channel_button: 'Crea un nuevo canal', 75 | channel_browser_joinable_channels_list_title: 'Canales a los que puedes unirte', 76 | channel_browser_joined_channels_list_title: 'Canales a los que perteneces', 77 | channel_browser_title: 'Explorar canales', 78 | channel_members_list_header: 'Miembros del canal', 79 | channel_panel_header: 'Detalles del canal', 80 | channel_topic_placeholder: 'Haga clic para agregar un tema', 81 | leave_channel_button: 'Salir de este canal', 82 | sidebarlist_channels: 'Canales', 83 | sidebarlist_favorites: 'Favoritas', 84 | sidebarlist_peers: 'Compañeras', 85 | theme_editor_screen_title: 'Theme Editor', 86 | }, 87 | } 88 | 89 | i18n.fallbacks = true 90 | i18n.translations = Translations 91 | 92 | export default Translations 93 | -------------------------------------------------------------------------------- /utils/cabal-main-ipc.js: -------------------------------------------------------------------------------- 1 | import Client from 'cabal-client' 2 | 3 | // const { ipcMain } = require('electron') 4 | // ipc = electron.ipcMain 5 | 6 | const DEFAULT_CHANNEL = 'default' 7 | const HOME_DIR = '' // TODO 8 | const DATA_DIR = '' // TODO 9 | const STATE_FILE = '' // TODO 10 | const DEFAULT_PAGE_SIZE = 100 11 | const MAX_FEEDS = 1000 12 | 13 | let client 14 | let loadedCabalKeys = [] 15 | 16 | export const initializeClient = ({ dataDir, maxFeeds, eventEmitter }) => { 17 | client = new Client({ 18 | maxFeeds: maxFeeds || MAX_FEEDS, 19 | config: { 20 | dbdir: dataDir || DATA_DIR, 21 | }, 22 | }) 23 | 24 | eventEmitter.on('render:add-message', ({ cabalKey, message }) => { 25 | const cabalDetails = client.getDetails(cabalKey) 26 | cabalDetails.publishMessage(message) 27 | }) 28 | 29 | eventEmitter.on('render:add-status-message', ({ cabalKey, channel, text }) => { 30 | const cabalDetails = client.getDetails(cabalKey) 31 | channel = channel || cabalDetails.getCurrentChannel() 32 | client.addStatusMessage({ text }, channel, cabalDetails._cabal) 33 | }) 34 | 35 | eventEmitter.on('render:cabal-focus', ({ cabalKey, channel }) => { 36 | client.focusCabal(cabalKey) 37 | }) 38 | 39 | eventEmitter.on('render:cabal-join', ({ cabalKey, channel }) => { 40 | console.log('main', 'render:cabal-join', cabalKey) 41 | initializeCabal({ cabalKey, channel, eventEmitter }) 42 | }) 43 | 44 | eventEmitter.on('render:channel-focus', ({ cabalKey, channel }) => { 45 | const cabalDetails = client.getDetails(cabalKey) 46 | cabalDetails.focusChannel(channel) 47 | console.log('main', 'render:channel-focus', { cabalKey, channel }) 48 | }) 49 | 50 | eventEmitter.on('render:channel-join', ({ cabalKey, channel }) => { 51 | console.log('main', 'render:channel-join', { cabalKey, channel }) 52 | if (channel && channel.length > 0) { 53 | const cabalDetails = client.getDetails(cabalKey) 54 | cabalDetails.joinChannel(channel) 55 | // TODO: 56 | // addChannel({ cabalKey, channel }) 57 | // viewChannel({ cabalKey, channel }) 58 | } 59 | }) 60 | 61 | eventEmitter.on('render:channel-leave', (props) => {}) 62 | 63 | eventEmitter.on('render:remove-cabal', (props) => {}) 64 | 65 | eventEmitter.on('render:set-channel-topic', (props) => {}) 66 | 67 | eventEmitter.on('render:set-username', ({ cabalKey, username }) => { 68 | console.log('main', 'render:set-username', { cabalKey, username }) 69 | const cabalDetails = client.getDetails(cabalKey) 70 | const currentUsername = cabalDetails.getLocalName() 71 | if (username !== currentUsername) { 72 | cabalDetails.publishNick(username) 73 | } 74 | }) 75 | 76 | console.log('--> MainIpc: initialized') 77 | return client 78 | } 79 | 80 | export const initializeCabal = async ({ cabalKey, nickname, settings, eventEmitter }) => { 81 | const isNew = !cabalKey 82 | let cabalDetails 83 | if (loadedCabalKeys.includes(cabalKey)) { 84 | cabalDetails = client.getDetails(cabalKey) 85 | } else { 86 | cabalDetails = isNew ? await client.createCabal() : await client.addCabal(cabalKey) 87 | cabalKey = cabalDetails.key 88 | loadedCabalKeys.push(cabalKey) 89 | } 90 | 91 | const cabalDetailsEvents = [ 92 | { 93 | name: 'cabal-focus', 94 | action: () => {}, 95 | }, 96 | { 97 | name: 'channel-focus', 98 | action: () => { 99 | eventEmitter.emit('main:channel-focus', { 100 | cabalKey, 101 | data: { 102 | channelsJoined: cabalDetails.getJoinedChannels(), 103 | channelMembers: cabalDetails.getChannelMembers(), 104 | // channelMessagesUnread: getCabalUnreadMessagesCount(cabalDetails), 105 | currentChannel: cabalDetails.getCurrentChannel(), 106 | username: cabalDetails.getLocalName(), 107 | users: cabalDetails.getUsers(), 108 | }, 109 | }) 110 | // dispatch(updateAllsChannelsUnreadCount({ addr, channelMessagesUnread })) 111 | }, 112 | }, 113 | { 114 | name: 'channel-join', 115 | action: () => { 116 | eventEmitter.emit('main:channel-join', { 117 | cabalKey, 118 | data: { 119 | channelMembers: cabalDetails.getChannelMembers(), 120 | // channelMessagesUnread: getCabalUnreadMessagesCount(cabalDetails), 121 | channelsJoined: cabalDetails.getJoinedChannels(), 122 | currentChannel: cabalDetails.getCurrentChannel(), 123 | }, 124 | }) 125 | // dispatch(getMessages({ addr, amount: 1000, channel: currentChannel })) 126 | // dispatch(updateAllsChannelsUnreadCount({ addr, channelMessagesUnread })) 127 | }, 128 | }, 129 | { 130 | name: 'channel-leave', 131 | action: () => { 132 | eventEmitter.emit('main:channel-leave', { 133 | cabalKey, 134 | data: { 135 | // channelMessagesUnread: getCabalUnreadMessagesCount(cabalDetails), 136 | channelsJoined: cabalDetails.getJoinedChannels(), 137 | }, 138 | }) 139 | // dispatch(updateAllsChannelsUnreadCount({ addr, channelMessagesUnread })) 140 | }, 141 | }, 142 | { 143 | name: 'init', 144 | action: () => { 145 | setTimeout(() => { 146 | eventEmitter.emit('main:init', { 147 | cabalKey, 148 | data: { 149 | users: cabalDetails.getUsers(), 150 | username: cabalDetails.getLocalName(), 151 | channels: cabalDetails.getChannels(), 152 | channelsJoined: cabalDetails.getJoinedChannels() || [], 153 | // channelMessagesUnread: getCabalUnreadMessagesCount(cabalDetails), 154 | currentChannel: cabalDetails.getCurrentChannel(), 155 | channelMembers: cabalDetails.getChannelMembers(), 156 | }, 157 | }) 158 | 159 | // dispatch(getMessages({ addr, amount: 1000, channel: currentChannel })) 160 | // dispatch(updateAllsChannelsUnreadCount({ addr, channelMessagesUnread })) 161 | }, 2000) 162 | }, 163 | }, 164 | { 165 | name: 'new-channel', 166 | action: () => { 167 | eventEmitter.emit('main:new-channel', { 168 | cabalKey, 169 | data: { 170 | channels: cabalDetails.getChannels(), 171 | channelMembers: cabalDetails.getChannelMembers(), 172 | }, 173 | }) 174 | }, 175 | }, 176 | { 177 | name: 'new-message', 178 | action: (data) => { 179 | eventEmitter.emit('main:new-message', { 180 | cabalKey, 181 | data, 182 | }) 183 | // dispatch(getMessages({ addr, amount: 1000, channel: currentChannel })) 184 | // dispatch(updateAllsChannelsUnreadCount({ addr, channelMessagesUnread })) 185 | }, 186 | }, 187 | { 188 | name: 'publish-message', 189 | action: () => { 190 | eventEmitter.emit('main:publish-message', { 191 | cabalKey, 192 | data: { 193 | // channelMessagesUnread: getCabalUnreadMessagesCount(cabalDetails), 194 | currentChannel: cabalDetails.getCurrentChannel(), 195 | }, 196 | }) 197 | // dispatch(getMessages({ addr, amount: 1000, channel: currentChannel })) 198 | // dispatch(updateAllsChannelsUnreadCount({ addr, channelMessagesUnread })) 199 | }, 200 | }, 201 | { 202 | name: 'publish-nick', 203 | action: () => { 204 | eventEmitter.emit('main:publish-nick', { 205 | cabalKey, 206 | data: { 207 | users: cabalDetails.getUsers(), 208 | }, 209 | }) 210 | }, 211 | }, 212 | { 213 | name: 'started-peering', 214 | action: () => { 215 | eventEmitter.emit('main:started-peering', { 216 | cabalKey, 217 | data: { 218 | users: cabalDetails.getUsers(), 219 | }, 220 | }) 221 | }, 222 | }, 223 | { 224 | name: 'status-message', 225 | action: () => { 226 | eventEmitter.emit('main:status-message', { 227 | cabalKey, 228 | data: { 229 | // channelMessagesUnread: getCabalUnreadMessagesCount(cabalDetails), 230 | currentChannel: cabalDetails.getCurrentChannel(), 231 | }, 232 | }) 233 | // dispatch(getMessages({ addr, amount: 1000, channel: currentChannel })) 234 | // dispatch(updateAllsChannelsUnreadCount({ addr, channelMessagesUnread })) 235 | }, 236 | }, 237 | { 238 | name: 'stopped-peering', 239 | action: () => { 240 | eventEmitter.emit('main:stopped-peering', { 241 | cabalKey, 242 | data: { 243 | users: cabalDetails.getUsers(), 244 | }, 245 | }) 246 | }, 247 | }, 248 | { 249 | name: 'topic', 250 | action: () => { 251 | eventEmitter.emit('main:topic', { 252 | cabalKey, 253 | data: { 254 | topic: cabalDetails.getTopic(), 255 | }, 256 | }) 257 | }, 258 | }, 259 | { 260 | name: 'user-updated', 261 | action: () => { 262 | eventEmitter.emit('main:user-updated', { 263 | cabalKey, 264 | data: { 265 | users: cabalDetails.getUsers(), 266 | }, 267 | }) 268 | }, 269 | }, 270 | ] 271 | cabalDetailsEvents.forEach((event) => { 272 | cabalDetails.on(event.name, (data) => { 273 | event.action(data) 274 | }) 275 | }) 276 | 277 | const outgoingCabalDetailsEvents = [ 278 | 'main:cabal-focus', 279 | 'main:channel-focus', 280 | 'main:channel-join', 281 | 'main:channel-leave', 282 | 'main:new-channel', 283 | 'main:new-message', 284 | 'main:publish-message', 285 | 'main:publish-nick', 286 | 'main:started-peering', 287 | 'main:status-message', 288 | 'main:stopped-peering', 289 | 'main:topic', 290 | 'main:user-updated', 291 | ] 292 | outgoingCabalDetailsEvents.forEach((eventName) => { 293 | cabalDetails.on(eventName, (data) => { 294 | console.log('--> event:' + eventName) 295 | eventEmitter.emit(eventName, data) 296 | }) 297 | }) 298 | 299 | client.focusCabal(cabalKey) 300 | 301 | setTimeout(() => { 302 | eventEmitter.emit('main:init', { 303 | cabalKey, 304 | data: { 305 | users: cabalDetails.getUsers(), 306 | username: cabalDetails.getLocalName(), 307 | channels: cabalDetails.getChannels(), 308 | channelsJoined: cabalDetails.getJoinedChannels() || [], 309 | // channelMessagesUnread: getCabalUnreadMessagesCount(cabalDetails), 310 | currentChannel: cabalDetails.getCurrentChannel(), 311 | channelMembers: cabalDetails.getChannelMembers(), 312 | }, 313 | }) 314 | 315 | // dispatch(getMessages({ addr, amount: 1000, channel: currentChannel })) 316 | // dispatch(updateAllsChannelsUnreadCount({ addr, channelMessagesUnread })) 317 | }, 2000) 318 | } 319 | 320 | export default { 321 | initializeClient, 322 | } 323 | -------------------------------------------------------------------------------- /utils/cabal-render-ipc.js: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react' 2 | import { useDispatch } from 'react-redux' 3 | 4 | import { 5 | addCabal, 6 | setCurrentCabal, 7 | setCurrentChannel 8 | } from '../features/cabals/cabalsSlice' 9 | import { 10 | addMessage 11 | } from '../features/cabals/messagesSlice' 12 | 13 | export function useRenderIpc ({ cabalKey, settings }) { 14 | const dispatch = useDispatch() 15 | 16 | useEffect(() => { 17 | const incomingEvents = [{ 18 | name: 'main:init', 19 | action: ({ cabalKey, data }) => { 20 | // focusCabal({ cabalKey, channel: settings.currentChannel }) 21 | console.log('main:init', data) 22 | 23 | const users = Object.values(data.users) 24 | 25 | dispatch(addCabal({ 26 | channelMembers: data.channelMembers, 27 | channels: data.channels, 28 | channelsJoined: data.channelsJoined, 29 | channelMessagesUnread: data.channelMessagesUnread, 30 | currentChannel: data.currentChannel, 31 | key: cabalKey, 32 | // messages: Array, 33 | username: data.username, 34 | users: users 35 | })) 36 | dispatch(setCurrentCabal(cabalKey)) 37 | dispatch(setCurrentChannel({ cabalKey, channel: data.currentChannel })) 38 | 39 | setUsername({ cabalKey, username: 'nickwarner-web' }) 40 | } 41 | }, { 42 | name: 'main:cabal-focus', 43 | action: (data) => { 44 | // focusCabal({ cabalKey, channel: settings.currentChannel }) 45 | console.log('main:cabal-focus', data) 46 | // setCurrentChannel(settings.currentChannel) 47 | } 48 | }, { 49 | name: 'main:channel-focus', 50 | action: (data) => { 51 | console.log('main:channel-focus ===>', data.currentChannel, data) //{ channelsJoined, channelMembers, currentChannel, username, users } 52 | dispatch(setCurrentChannel({ cabalKey, channel: data.currentChannel })) 53 | } 54 | }, { 55 | name: 'main:channel-join', 56 | action: ({ channelsJoined, channelMembers, currentChannel }) => { 57 | dispatch(setCurrentChannel({ cabalKey, channel: currentChannel })) 58 | } 59 | }, { 60 | name: 'main:channel-leave', 61 | action: (props) => { 62 | 63 | } 64 | }, { 65 | name: 'main:new-channel', 66 | action: (props) => { 67 | 68 | } 69 | }, { 70 | name: 'main:new-message', 71 | action: (props) => { 72 | console.log('main:new-message', props) 73 | dispatch(addMessage(props)) 74 | } 75 | }, { 76 | name: 'main:publish-message', 77 | action: (props) => { 78 | 79 | } 80 | }, { 81 | name: 'main:publish-nick', 82 | action: ({ name }) => { 83 | dispatch(publishStatusMessage({ 84 | cabalKey, 85 | text: `Nick set to: ${name}` 86 | })) 87 | } 88 | }, { 89 | name: 'main:started-peering', 90 | action: (props) => { 91 | 92 | } 93 | }, { 94 | name: 'main:status-message', 95 | action: (props) => { 96 | 97 | } 98 | }, { 99 | name: 'main:stopped-peering', 100 | action: (props) => { 101 | 102 | } 103 | }, { 104 | name: 'main:topic', 105 | action: (props) => { 106 | 107 | } 108 | }, { 109 | name: 'main:user-updated', 110 | action: (props) => { 111 | 112 | } 113 | }] 114 | 115 | incomingEvents.forEach(({ name, action }) => { 116 | window.eventEmitter.on(name, action) 117 | }) 118 | }, []) 119 | } 120 | 121 | export const publishMessage = ({ cabalKey, channel, message }) => { 122 | window.eventEmitter.emit('render:add-message', { cabalKey, channel, message }) 123 | } 124 | 125 | export const publishStatusMessage = ({ cabalKey, channel, text }) => { 126 | window.eventEmitter.emit('render:add-status-message', { cabalKey, channel, text }) 127 | } 128 | 129 | export const focusCabal = ({ cabalKey, channel }) => { 130 | window.eventEmitter.emit('render:cabal-focus', { cabalKey, channel }) 131 | } 132 | 133 | export const focusChannel = ({ cabalKey, channel }) => { 134 | cabalKey = cabalKey || cabalKey 135 | window.eventEmitter.emit('render:channel-focus', { cabalKey, channel }) 136 | } 137 | 138 | export const joinChannel = ({ cabalKey, channel }) => { 139 | window.eventEmitter.emit('render:channel-join', { cabalKey, channel }) 140 | } 141 | 142 | export const setUsername = ({ cabalKey, username }) => { 143 | window.eventEmitter.emit('render:set-username', { cabalKey, username }) 144 | } 145 | 146 | export const leaveChannel = ({ cabalKey, channel }) => { 147 | window.eventEmitter.emit('render:channel-leave', { cabalKey, channel }) 148 | } 149 | 150 | export const removeCabal = ({ cabalKey }) => { 151 | window.eventEmitter.emit('render:remove-cabal', { cabalKey }) 152 | } 153 | 154 | export const setChannelTopic = ({ cabalKey, channel, topic }) => { 155 | window.eventEmitter.emit('render:set-channel-topic', { cabalKey, channel, topic }) 156 | } 157 | -------------------------------------------------------------------------------- /utils/cabal-websocket-client.js: -------------------------------------------------------------------------------- 1 | export default function CabalWebsocketClient ({ cabalKey, websocketURL }) { 2 | if (!cabalKey) { 3 | const url = new URL(window.location.href) 4 | url.protocol = url.protocol.replace('https:', 'wss:').replace('http:', 'ws:') 5 | const parts = window.location.href.split('/') 6 | cabalKey = parts[parts.length - 2] 7 | websocketURL = url.href 8 | } 9 | 10 | function init () { 11 | console.log('websocket init', websocketURL) 12 | const websocket = new window.WebSocket(websocketURL) 13 | websocket.onopen = function () { 14 | websocket.send(JSON.stringify({ 15 | type: 'render:cabal-join', 16 | data: { 17 | cabalKey: cabalKey 18 | } 19 | })) 20 | } 21 | 22 | websocket.onclose = function (evt) { console.log(evt) } 23 | websocket.onerror = function (evt) { console.warn(evt) } 24 | 25 | // Incoming messages from main 26 | websocket.onmessage = function (msg) { 27 | const message = JSON.parse(msg.data) 28 | console.log('from main -->', message) 29 | if (message.type) { 30 | window.eventEmitter.emit(message.type, message.data) 31 | } 32 | } 33 | 34 | // Outgoing messages to main 35 | const outgoingMessages = [ 36 | 'render:add-message', 37 | 'render:add-status-message', 38 | 'render:cabal-focus', 39 | 'render:channel-focus', 40 | 'render:channel-join', 41 | 'render:channel-leave', 42 | 'render:remove-cabal', 43 | 'render:set-channel-topic', 44 | 'render:set-username' 45 | ] 46 | outgoingMessages.forEach((eventName) => { 47 | window.eventEmitter.on(eventName, (data) => { 48 | websocket.send(JSON.stringify({ 49 | type: eventName, 50 | data 51 | })) 52 | }) 53 | }) 54 | } 55 | 56 | window.addEventListener('load', init, false) 57 | } 58 | -------------------------------------------------------------------------------- /utils/fakeData.ts: -------------------------------------------------------------------------------- 1 | import { CabalProps, ChannelProps, MessageProps, UserProps } from '../app/types' 2 | 3 | export const defaultUser: UserProps = { 4 | key: '1234', 5 | name: 'person', 6 | online: true, 7 | } 8 | 9 | export const defaultUsers: UserProps[] = [ 10 | defaultUser, 11 | { ...defaultUser, key: '1235', name: 'friend' }, 12 | { ...defaultUser, key: '1236', name: 'ally' }, 13 | { ...defaultUser, key: '1237', name: 'pal', online: false }, 14 | { ...defaultUser, key: '1238', name: 'conspirator', online: false }, 15 | ] 16 | 17 | export const manyUsers: UserProps[] = [ 18 | ...defaultUsers, 19 | ...defaultUsers, 20 | ...defaultUsers, 21 | ...defaultUsers, 22 | ...defaultUsers, 23 | ] 24 | 25 | export const message: MessageProps = { 26 | content: 'Hello friends!', 27 | key: '1232', 28 | timestamp: new Date(2019, 8, 21, 12, 0).toISOString(), 29 | user: defaultUser, 30 | } 31 | 32 | export const longMessage: MessageProps = { 33 | content: 34 | 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.', 35 | key: '1232', 36 | timestamp: new Date(2019, 8, 21, 12, 0).toISOString(), 37 | user: defaultUser, 38 | } 39 | 40 | export const longMessageOtherUser: MessageProps = { 41 | ...longMessage, 42 | user: defaultUsers[1], 43 | } 44 | 45 | export const defaultMessages: MessageProps[] = [ 46 | { 47 | content: 'Hello friends!', 48 | key: '1234', 49 | timestamp: new Date(2019, 8, 20, 12, 0).toISOString(), 50 | user: defaultUser, 51 | }, 52 | { 53 | content: 'welcome to cabal :D', 54 | key: '1235', 55 | timestamp: new Date(2019, 8, 21, 12, 1).toISOString(), 56 | user: defaultUsers[1], 57 | }, 58 | { 59 | content: 'person: Thanks! So happy to be here! ❤️', 60 | key: '1236', 61 | timestamp: new Date(2019, 8, 21, 12, 2).toISOString(), 62 | user: defaultUsers[2], 63 | }, 64 | ] 65 | 66 | export const manyMessages: MessageProps[] = [ 67 | ...defaultMessages, 68 | longMessage, 69 | ...defaultMessages, 70 | ...defaultMessages, 71 | longMessage, 72 | ...defaultMessages, 73 | ...defaultMessages, 74 | longMessage, 75 | ...defaultMessages, 76 | ] 77 | 78 | export const defaultChannel: ChannelProps = { 79 | members: defaultUsers, 80 | name: 'default', 81 | topic: 'welcome to cabal - see https://cabal.chat for more information', 82 | } 83 | 84 | export const defaultChannels: ChannelProps[] = [ 85 | defaultChannel, 86 | { ...defaultChannel, name: 'arts' }, 87 | { ...defaultChannel, name: 'crafts' }, 88 | { ...defaultChannel, name: 'dance' }, 89 | { ...defaultChannel, name: 'film' }, 90 | { ...defaultChannel, name: 'music' }, 91 | { ...defaultChannel, name: 'solarpunk' }, 92 | { ...defaultChannel, name: 'the galley' }, 93 | ] 94 | 95 | export const manyChannels: ChannelProps[] = [ 96 | ...defaultChannels, 97 | ...defaultChannels, 98 | ...defaultChannels, 99 | ...defaultChannels, 100 | ...defaultChannels, 101 | ] 102 | 103 | export const defaultCabal: CabalProps = { 104 | channels: defaultChannels, 105 | channelsJoined: defaultChannels.slice(0, 3), 106 | channelsFavorites: [defaultChannels[1]], 107 | currentChannel: defaultChannels[0], 108 | id: '0201400f1aa2e3076a3f17f4521b2cc41e258c446cdaa44742afe6e1b9fd5f82', 109 | key: '0201400f1aa2e3076a3f17f4521b2cc41e258c446cdaa44742afe6e1b9fd5f82', 110 | name: 'Cabal Club', 111 | username: 'nickwarner', 112 | users: defaultUsers, 113 | } 114 | 115 | export const defaultCabals: CabalProps[] = [ 116 | defaultCabal, 117 | { 118 | ...defaultCabal, 119 | key: '1fdc83d08699781adfeacba9aa6bb880203d5e61357e5667ccbcc12e4a9065ad', 120 | }, 121 | { ...defaultCabal, id: '7c6f63f92765e36102', key: '7c6f63f92765e36102' }, 122 | { ...defaultCabal, id: '04b51eef9ad64e2841', key: '04b51eef9ad64e2841' }, 123 | { ...defaultCabal, id: 'f63f92765e36102046', key: 'f63f92765e36102046' }, 124 | { ...defaultCabal, id: '521b2cc41e258c446c', key: '521b2cc41e258c446c' }, 125 | ] 126 | 127 | export const manyCabals: CabalProps[] = [ 128 | ...defaultCabals, 129 | ...defaultCabals, 130 | ...defaultCabals, 131 | ...defaultCabals, 132 | ...defaultCabals, 133 | ] 134 | -------------------------------------------------------------------------------- /utils/helpers.js: -------------------------------------------------------------------------------- 1 | export const generateUniqueName = () => { 2 | const adjectives = ['ancient', 'whispering', 'hidden', 'emerald', 'occult', 'obscure', 'wandering', 'ephemeral', 'eccentric', 'singing'] 3 | const nouns = ['lichen', 'moss', 'shadow', 'stone', 'ghost', 'friend', 'spore', 'fungi', 'mold', 'mountain', 'compost', 'conspirator'] 4 | 5 | const randomItem = (array) => array[Math.floor(Math.random() * array.length)] 6 | return `${randomItem(adjectives)}-${randomItem(nouns)}` 7 | } 8 | 9 | export default { 10 | generateUniqueName 11 | } 12 | -------------------------------------------------------------------------------- /utils/textInputCommands.js: -------------------------------------------------------------------------------- 1 | import { 2 | setUsername, 3 | joinChannel, 4 | leaveChannel, 5 | removeCabal, 6 | setChannelTopic, 7 | saveCabalSettings 8 | } from '../features/cabals/cabalsSlice' 9 | import { 10 | publishMessage, 11 | publishStatusMessage 12 | } from '../features/cabals/messagesSlice' 13 | 14 | export const listCommands = (cabalKey) => { 15 | const commands = { 16 | help: { 17 | help: () => 'display this help message', 18 | call: (arg) => { 19 | var helpContent = '' 20 | for (var key in commands) { 21 | helpContent = helpContent + `/${key} - ${commands[key].help()} \n` 22 | } 23 | publishStatusMessage({ cabalKey, text: helpContent }) 24 | } 25 | }, 26 | nick: { 27 | help: () => 'change your display name', 28 | call: (arg) => { 29 | var username = arg 30 | if (!username.length) return 31 | if (username && username.trim().length > 0) { 32 | setUsername({ cabalKey, username }) 33 | } 34 | } 35 | }, 36 | emote: { 37 | help: () => 'write an old-school text emote', 38 | call: (arg) => { 39 | publishMessage({ 40 | text: arg, 41 | type: 'chat/emote' 42 | }) 43 | } 44 | }, 45 | join: { 46 | help: () => 'join a new channel', 47 | call: (arg) => { 48 | var channel = arg || 'default' 49 | joinChannel({ cabalKey, channel }) 50 | } 51 | }, 52 | leave: { 53 | help: () => 'leave a channel', 54 | call: (arg) => { 55 | var channel = arg 56 | leaveChannel({ cabalKey, channel }) 57 | } 58 | }, 59 | // quit: { 60 | // help: () => 'exit the cabal process', 61 | // call: (arg) => { 62 | // // TODO 63 | // // process.exit(0) 64 | // } 65 | // }, 66 | topic: { 67 | help: () => 'set the topic/description/"message of the day" for a channel', 68 | call: (arg) => { 69 | var topic = arg 70 | if (topic && topic.trim().length > 0) { 71 | setChannelTopic({ cabalKey, topic }) 72 | } 73 | } 74 | }, 75 | // whoami: { 76 | // help: () => 'display your local user key', 77 | // call: (arg) => { 78 | // // TODO 79 | // // view.writeLine.bind(view)('Local user key: ' + cabal.client.user.key) 80 | // } 81 | // }, 82 | alias: { 83 | help: () => 'set alias for the cabal', 84 | call: (arg) => { 85 | saveCabalSettings({ 86 | cabalKey, 87 | settings: { 88 | alias: arg 89 | } 90 | }) 91 | } 92 | }, 93 | // add: { 94 | // help: () => 'add a cabal', 95 | // call: (arg) => { 96 | // addAnotherCabal(arg) 97 | // } 98 | // }, 99 | remove: { 100 | help: () => 'remove cabal from Cabal Desktop', 101 | call: (arg) => { 102 | cabalKey = arg || cabalKey 103 | removeCabal({ cabalKey }) 104 | } 105 | } 106 | } 107 | 108 | const alias = (command, alias) => { 109 | commands[alias] = { 110 | help: commands[command].help, 111 | call: commands[command].call 112 | } 113 | } 114 | 115 | // add aliases to commands 116 | alias('emote', 'me') 117 | alias('join', 'j') 118 | alias('nick', 'n') 119 | alias('topic', 'motd') 120 | // alias('whoami', 'key') 121 | 122 | return commands 123 | } 124 | 125 | export const processCommand = ({ cabalKey, text }) => { 126 | const commands = listCommands(cabalKey) 127 | 128 | const pattern = (/^\/(\w*)\s*(.*)/) 129 | const m = pattern.exec(text) || [] 130 | const cmd = m[1] ? m[1].trim() : '' 131 | const arg = m[2] ? m[2].trim() : '' 132 | 133 | if (cmd in commands) { 134 | commands[cmd].call(arg) 135 | } else if (cmd) { 136 | text = `/${cmd} is not yet a command. \nTry /help for a list of command descriptions` 137 | publishStatusMessage({ cabalKey, text }) 138 | } 139 | } 140 | 141 | export default { 142 | listCommands, 143 | processCommand 144 | } 145 | -------------------------------------------------------------------------------- /web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 14 | %WEB_TITLE% 15 | 63 | 64 | 65 | 66 | 67 | 68 | 72 | 73 | 85 | 94 | Oh no! It looks like JavaScript is not enabled in your browser. 95 | 96 | 110 | Reload 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | --------------------------------------------------------------------------------
Oh no! It looks like JavaScript is not enabled in your browser.
96 | 110 | Reload 111 | 112 |