├── .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 |