├── .eslintignore
├── .yarnrc.yml
├── amplify
├── backend
│ ├── storage
│ │ └── s39381675c
│ │ │ ├── storage-params.json
│ │ │ ├── parameters.json
│ │ │ └── s3-cloudformation-template.json
│ ├── function
│ │ └── SignalClonePostConfirmation
│ │ │ ├── parameters.json
│ │ │ ├── src
│ │ │ ├── event.json
│ │ │ ├── package.json
│ │ │ ├── package-lock.json
│ │ │ ├── custom.js
│ │ │ └── index.js
│ │ │ ├── amplify.state
│ │ │ ├── function-parameters.json
│ │ │ └── SignalClonePostConfirmation-cloudformation-template.json
│ ├── tags.json
│ ├── api
│ │ └── SignalClone
│ │ │ ├── transform.conf.json
│ │ │ ├── parameters.json
│ │ │ ├── resolvers
│ │ │ └── README.md
│ │ │ ├── schema.graphql
│ │ │ └── stacks
│ │ │ └── CustomResources.json
│ ├── backend-config.json
│ └── auth
│ │ └── SignalClone
│ │ ├── parameters.json
│ │ ├── auth-trigger-cloudformation-template.json
│ │ └── SignalClone-cloudformation-template.yml
├── .config
│ └── project-config.json
├── team-provider-info.json
└── cli.json
├── components
├── Message
│ ├── index.ts
│ └── Message.tsx
├── UserItem
│ ├── index.ts
│ ├── styles.ts
│ └── UserItem.tsx
├── ChatRoomItem
│ ├── index.ts
│ ├── styles.ts
│ └── ChatRoomItem.tsx
├── MessageInput
│ ├── index.ts
│ └── MessageInput.tsx
├── MessageReply
│ ├── index.ts
│ └── MessageReply.tsx
├── AudioPlayer
│ ├── index.tsx
│ └── AudioPlayer.tsx
├── StyledText.tsx
├── __tests__
│ └── StyledText-test.js
├── NewGroupButton
│ └── index.tsx
├── Themed.tsx
└── EditScreenInfo.tsx
├── screens
├── GroupInfoScreen
│ ├── index.ts
│ └── GroupInfoScreen.tsx
├── TabTwoScreen.tsx
├── NotFoundScreen.tsx
├── HomeScreen.tsx
├── Settings.tsx
├── ChatRoomScreen.tsx
└── UsersScreen.tsx
├── assets
├── images
│ ├── icon.png
│ ├── favicon.png
│ ├── splash.png
│ └── adaptive-icon.png
├── fonts
│ └── SpaceMono-Regular.ttf
└── dummy-data
│ ├── Users.ts
│ ├── Chats.ts
│ └── ChatRooms.ts
├── src
└── models
│ ├── schema.d.ts
│ ├── index.js
│ ├── index.d.ts
│ └── schema.js
├── tsconfig.json
├── babel.config.js
├── metro.config.js
├── constants
├── Layout.ts
└── Colors.ts
├── .expo-shared
└── assets.json
├── .vscode
└── settings.json
├── hooks
├── useColorScheme.ts
└── useCachedResources.ts
├── types.tsx
├── .gitignore
├── navigation
├── LinkingConfiguration.ts
├── ChatRoomHeader.tsx
└── index.tsx
├── app.json
├── LICENSE
├── package.json
├── utils
└── crypto.js
└── App.tsx
/.eslintignore:
--------------------------------------------------------------------------------
1 | src/models
--------------------------------------------------------------------------------
/.yarnrc.yml:
--------------------------------------------------------------------------------
1 | nodeLinker: node-modules
2 |
--------------------------------------------------------------------------------
/amplify/backend/storage/s39381675c/storage-params.json:
--------------------------------------------------------------------------------
1 | {}
--------------------------------------------------------------------------------
/components/Message/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './Message';
--------------------------------------------------------------------------------
/components/UserItem/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './UserItem';
--------------------------------------------------------------------------------
/components/ChatRoomItem/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './ChatRoomItem';
--------------------------------------------------------------------------------
/components/MessageInput/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './MessageInput';
--------------------------------------------------------------------------------
/components/MessageReply/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './MessageReply';
--------------------------------------------------------------------------------
/components/AudioPlayer/index.tsx:
--------------------------------------------------------------------------------
1 | export { default } from "./AudioPlayer";
2 |
--------------------------------------------------------------------------------
/screens/GroupInfoScreen/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './GroupInfoScreen';
--------------------------------------------------------------------------------
/assets/images/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/VadimNotJustDev/SignalClone/HEAD/assets/images/icon.png
--------------------------------------------------------------------------------
/assets/images/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/VadimNotJustDev/SignalClone/HEAD/assets/images/favicon.png
--------------------------------------------------------------------------------
/assets/images/splash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/VadimNotJustDev/SignalClone/HEAD/assets/images/splash.png
--------------------------------------------------------------------------------
/src/models/schema.d.ts:
--------------------------------------------------------------------------------
1 | import { Schema } from '@aws-amplify/datastore';
2 |
3 | export declare const schema: Schema;
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "expo/tsconfig.base",
3 | "compilerOptions": {
4 | "strict": true
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/assets/images/adaptive-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/VadimNotJustDev/SignalClone/HEAD/assets/images/adaptive-icon.png
--------------------------------------------------------------------------------
/assets/fonts/SpaceMono-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/VadimNotJustDev/SignalClone/HEAD/assets/fonts/SpaceMono-Regular.ttf
--------------------------------------------------------------------------------
/amplify/backend/function/SignalClonePostConfirmation/parameters.json:
--------------------------------------------------------------------------------
1 | {
2 | "modules": "custom",
3 | "resourceName": "SignalClonePostConfirmation"
4 | }
--------------------------------------------------------------------------------
/amplify/backend/function/SignalClonePostConfirmation/src/event.json:
--------------------------------------------------------------------------------
1 | {
2 | "request": {
3 | "userPoolId": "testID",
4 | "userName": "testUser"
5 | },
6 | "response": {}
7 | }
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = function(api) {
2 | api.cache(true);
3 | return {
4 | presets: ['babel-preset-expo'],
5 | plugins: ['react-native-reanimated/plugin'],
6 | };
7 | };
8 |
--------------------------------------------------------------------------------
/amplify/backend/tags.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "Key": "user:Stack",
4 | "Value": "{project-env}"
5 | },
6 | {
7 | "Key": "user:Application",
8 | "Value": "{project-name}"
9 | }
10 | ]
--------------------------------------------------------------------------------
/metro.config.js:
--------------------------------------------------------------------------------
1 | const blacklist = require("metro-config/src/defaults/blacklist");
2 |
3 | module.exports = {
4 | resolver: {
5 | blacklistRE: blacklist([/#current-cloud-backend\/.*/]),
6 | },
7 | };
--------------------------------------------------------------------------------
/amplify/backend/function/SignalClonePostConfirmation/amplify.state:
--------------------------------------------------------------------------------
1 | {
2 | "pluginId": "amplify-nodejs-function-runtime-provider",
3 | "functionRuntime": "nodejs",
4 | "useLegacyBuild": true,
5 | "defaultEditorFile": "src/index.js"
6 | }
--------------------------------------------------------------------------------
/components/StyledText.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | import { Text, TextProps } from './Themed';
4 |
5 | export function MonoText(props: TextProps) {
6 | return ;
7 | }
8 |
--------------------------------------------------------------------------------
/amplify/backend/api/SignalClone/transform.conf.json:
--------------------------------------------------------------------------------
1 | {
2 | "Version": 5,
3 | "ElasticsearchWarning": true,
4 | "ResolverConfig": {
5 | "project": {
6 | "ConflictHandler": "AUTOMERGE",
7 | "ConflictDetection": "VERSION"
8 | }
9 | }
10 | }
--------------------------------------------------------------------------------
/constants/Layout.ts:
--------------------------------------------------------------------------------
1 | import { Dimensions } from 'react-native';
2 |
3 | const width = Dimensions.get('window').width;
4 | const height = Dimensions.get('window').height;
5 |
6 | export default {
7 | window: {
8 | width,
9 | height,
10 | },
11 | isSmallDevice: width < 375,
12 | };
13 |
--------------------------------------------------------------------------------
/amplify/backend/api/SignalClone/parameters.json:
--------------------------------------------------------------------------------
1 | {
2 | "AppSyncApiName": "SignalClone",
3 | "DynamoDBBillingMode": "PAY_PER_REQUEST",
4 | "DynamoDBEnableServerSideEncryption": false,
5 | "AuthCognitoUserPoolId": {
6 | "Fn::GetAtt": [
7 | "authSignalClone",
8 | "Outputs.UserPoolId"
9 | ]
10 | }
11 | }
--------------------------------------------------------------------------------
/amplify/backend/function/SignalClonePostConfirmation/src/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "SignalClonePostConfirmation",
3 | "version": "2.0.0",
4 | "description": "Lambda function generated by Amplify",
5 | "main": "index.js",
6 | "license": "Apache-2.0",
7 | "dependencies": {
8 | "axios": "latest"
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/amplify/backend/api/SignalClone/resolvers/README.md:
--------------------------------------------------------------------------------
1 | Any resolvers that you add in this directory will override the ones automatically generated by Amplify CLI and will be directly copied to the cloud.
2 | For more information, visit [https://docs.amplify.aws/cli/graphql-transformer/resolvers](https://docs.amplify.aws/cli/graphql-transformer/resolvers)
--------------------------------------------------------------------------------
/.expo-shared/assets.json:
--------------------------------------------------------------------------------
1 | {
2 | "e997a5256149a4b76e6bfd6cbf519c5e5a0f1d278a3d8fa1253022b03c90473b": true,
3 | "af683c96e0ffd2cf81287651c9433fa44debc1220ca7cb431fe482747f34a505": true,
4 | "12bb71342c6255bbf50437ec8f4441c083f47cdb74bd89160c15e4f43e52a1cb": true,
5 | "40b842e832070c58deac6aa9e08fa459302ee3f9da492c7e77d93d2fbf4a56fd": true
6 | }
7 |
--------------------------------------------------------------------------------
/components/__tests__/StyledText-test.js:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import renderer from 'react-test-renderer';
3 |
4 | import { MonoText } from '../StyledText';
5 |
6 | it(`renders correctly`, () => {
7 | const tree = renderer.create(Snapshot test!).toJSON();
8 |
9 | expect(tree).toMatchSnapshot();
10 | });
11 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "files.exclude": {
3 | "amplify/.config": true,
4 | "amplify/**/*-parameters.json": true,
5 | "amplify/**/amplify.state": true,
6 | "amplify/**/transform.conf.json": true,
7 | "amplify/#current-cloud-backend": true,
8 | "amplify/backend/amplify-meta.json": true,
9 | "amplify/backend/awscloudformation": true
10 | }
11 | }
--------------------------------------------------------------------------------
/src/models/index.js:
--------------------------------------------------------------------------------
1 | // @ts-check
2 | import { initSchema } from '@aws-amplify/datastore';
3 | import { schema } from './schema';
4 |
5 | const MessageStatus = {
6 | "SENT": "SENT",
7 | "DELIVERED": "DELIVERED",
8 | "READ": "READ"
9 | };
10 |
11 | const { Message, ChatRoom, ChatRoomUser, User } = initSchema(schema);
12 |
13 | export {
14 | Message,
15 | ChatRoom,
16 | ChatRoomUser,
17 | User,
18 | MessageStatus
19 | };
--------------------------------------------------------------------------------
/amplify/.config/project-config.json:
--------------------------------------------------------------------------------
1 | {
2 | "providers": [
3 | "awscloudformation"
4 | ],
5 | "projectName": "SignalClone",
6 | "version": "3.1",
7 | "frontend": "javascript",
8 | "javascript": {
9 | "framework": "react-native",
10 | "config": {
11 | "SourceDir": "src",
12 | "DistributionDir": "/",
13 | "BuildCommand": "npm run-script build",
14 | "StartCommand": "npm run-script start"
15 | }
16 | }
17 | }
--------------------------------------------------------------------------------
/hooks/useColorScheme.ts:
--------------------------------------------------------------------------------
1 | import { ColorSchemeName, useColorScheme as _useColorScheme } from 'react-native';
2 |
3 | // The useColorScheme value is always either light or dark, but the built-in
4 | // type suggests that it can be null. This will not happen in practice, so this
5 | // makes it a bit easier to work with.
6 | export default function useColorScheme(): NonNullable {
7 | return _useColorScheme() as NonNullable;
8 | }
9 |
--------------------------------------------------------------------------------
/constants/Colors.ts:
--------------------------------------------------------------------------------
1 | const tintColorLight = '#2f95dc';
2 | const tintColorDark = '#fff';
3 |
4 | export default {
5 | light: {
6 | text: '#000',
7 | background: '#fff',
8 | tint: tintColorLight,
9 | tabIconDefault: '#ccc',
10 | tabIconSelected: tintColorLight,
11 | },
12 | dark: {
13 | text: '#fff',
14 | background: '#000',
15 | tint: tintColorDark,
16 | tabIconDefault: '#ccc',
17 | tabIconSelected: tintColorDark,
18 | },
19 | };
20 |
--------------------------------------------------------------------------------
/types.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Learn more about using TypeScript with React Navigation:
3 | * https://reactnavigation.org/docs/typescript/
4 | */
5 |
6 | export type RootStackParamList = {
7 | Root: undefined;
8 | NotFound: undefined;
9 | };
10 |
11 | export type BottomTabParamList = {
12 | TabOne: undefined;
13 | TabTwo: undefined;
14 | };
15 |
16 | export type TabOneParamList = {
17 | TabOneScreen: undefined;
18 | };
19 |
20 | export type TabTwoParamList = {
21 | TabTwoScreen: undefined;
22 | };
23 |
--------------------------------------------------------------------------------
/components/NewGroupButton/index.tsx:
--------------------------------------------------------------------------------
1 | import { FontAwesome } from "@expo/vector-icons";
2 | import React from "react";
3 | import { Pressable, View, Text } from "react-native";
4 |
5 | const NewGroupButton = ({ onPress }) => {
6 | return (
7 |
8 |
9 |
10 | New group
11 |
12 |
13 | );
14 | };
15 |
16 | export default NewGroupButton;
17 |
--------------------------------------------------------------------------------
/amplify/backend/function/SignalClonePostConfirmation/function-parameters.json:
--------------------------------------------------------------------------------
1 | {
2 | "trigger": true,
3 | "modules": [
4 | "custom"
5 | ],
6 | "parentResource": "SignalClone",
7 | "functionName": "SignalClonePostConfirmation",
8 | "resourceName": "SignalClonePostConfirmation",
9 | "parentStack": "auth",
10 | "triggerEnvs": [],
11 | "triggerDir": "/snapshot/node_modules/amplify-category-auth/provider-utils/awscloudformation/triggers/PostConfirmation",
12 | "triggerTemplate": "PostConfirmation.json.ejs",
13 | "triggerEventPath": "PostConfirmation.event.json",
14 | "roleName": "SignalClonePostConfirmation",
15 | "skipEdit": true,
16 | "enableCors": false
17 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | .expo/
3 | npm-debug.*
4 | *.jks
5 | *.p8
6 | *.p12
7 | *.key
8 | *.mobileprovision
9 | *.orig.*
10 | web-build/
11 |
12 | # macOS
13 | .DS_Store
14 |
15 | #amplify-do-not-edit-begin
16 | amplify/\#current-cloud-backend
17 | amplify/.config/local-*
18 | amplify/logs
19 | amplify/mock-data
20 | amplify/backend/amplify-meta.json
21 | amplify/backend/awscloudformation
22 | amplify/backend/.temp
23 | build/
24 | dist/
25 | node_modules/
26 | aws-exports.js
27 | awsconfiguration.json
28 | amplifyconfiguration.json
29 | amplifyconfiguration.dart
30 | amplify-build-config.json
31 | amplify-gradle-config.json
32 | amplifytools.xcconfig
33 | .secret-*
34 | #amplify-do-not-edit-end
35 |
--------------------------------------------------------------------------------
/navigation/LinkingConfiguration.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Learn more about deep linking with React Navigation
3 | * https://reactnavigation.org/docs/deep-linking
4 | * https://reactnavigation.org/docs/configuring-links
5 | */
6 |
7 | import * as Linking from 'expo-linking';
8 |
9 | export default {
10 | prefixes: [Linking.makeUrl('/')],
11 | config: {
12 | screens: {
13 | Root: {
14 | screens: {
15 | TabOne: {
16 | screens: {
17 | TabOneScreen: 'one',
18 | },
19 | },
20 | TabTwo: {
21 | screens: {
22 | TabTwoScreen: 'two',
23 | },
24 | },
25 | },
26 | },
27 | NotFound: '*',
28 | },
29 | },
30 | };
31 |
--------------------------------------------------------------------------------
/amplify/backend/function/SignalClonePostConfirmation/src/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "SignalClonePostConfirmation",
3 | "version": "2.0.0",
4 | "lockfileVersion": 1,
5 | "requires": true,
6 | "dependencies": {
7 | "axios": {
8 | "version": "0.23.0",
9 | "resolved": "https://registry.npmjs.org/axios/-/axios-0.23.0.tgz",
10 | "integrity": "sha512-NmvAE4i0YAv5cKq8zlDoPd1VLKAqX5oLuZKs8xkJa4qi6RGn0uhCYFjWtHHC9EM/MwOwYWOs53W+V0aqEXq1sg==",
11 | "requires": {
12 | "follow-redirects": "^1.14.4"
13 | }
14 | },
15 | "follow-redirects": {
16 | "version": "1.14.4",
17 | "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.4.tgz",
18 | "integrity": "sha512-zwGkiSXC1MUJG/qmeIFH2HBJx9u0V46QGUe3YR1fXG8bXQxq7fLj0RjLZQ5nubr9qNJUZrH+xUcwXEoXNpfS+g=="
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "expo": {
3 | "name": "SignalClone",
4 | "slug": "SignalClone",
5 | "version": "1.0.0",
6 | "orientation": "portrait",
7 | "icon": "./assets/images/icon.png",
8 | "scheme": "myapp",
9 | "userInterfaceStyle": "automatic",
10 | "splash": {
11 | "image": "./assets/images/splash.png",
12 | "resizeMode": "contain",
13 | "backgroundColor": "#ffffff"
14 | },
15 | "updates": {
16 | "fallbackToCacheTimeout": 0
17 | },
18 | "assetBundlePatterns": [
19 | "**/*"
20 | ],
21 | "ios": {
22 | "supportsTablet": true
23 | },
24 | "android": {
25 | "adaptiveIcon": {
26 | "foregroundImage": "./assets/images/adaptive-icon.png",
27 | "backgroundColor": "#ffffff"
28 | }
29 | },
30 | "web": {
31 | "favicon": "./assets/images/favicon.png"
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/screens/TabTwoScreen.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { StyleSheet } from 'react-native';
3 |
4 | import EditScreenInfo from '../components/EditScreenInfo';
5 | import { Text, View } from '../components/Themed';
6 |
7 | export default function TabTwoScreen() {
8 | return (
9 |
10 | Tab Two
11 |
12 |
13 |
14 | );
15 | }
16 |
17 | const styles = StyleSheet.create({
18 | container: {
19 | flex: 1,
20 | alignItems: 'center',
21 | justifyContent: 'center',
22 | },
23 | title: {
24 | fontSize: 20,
25 | fontWeight: 'bold',
26 | },
27 | separator: {
28 | marginVertical: 30,
29 | height: 1,
30 | width: '80%',
31 | },
32 | });
33 |
--------------------------------------------------------------------------------
/components/UserItem/styles.ts:
--------------------------------------------------------------------------------
1 | import { StyleSheet } from 'react-native';
2 |
3 | const styles = StyleSheet.create({
4 | container: {
5 | flexDirection: 'row',
6 | padding: 10,
7 | },
8 | image: {
9 | height: 50,
10 | width: 50,
11 | borderRadius: 30,
12 | marginRight: 10,
13 | },
14 | badgeContainer: {
15 | backgroundColor: '#3777f0',
16 | width: 20,
17 | height: 20,
18 | borderRadius: 10,
19 | borderWidth: 1,
20 | borderColor: 'white',
21 | justifyContent: 'center',
22 | alignItems: 'center',
23 | position: 'absolute',
24 | left: 45,
25 | top: 10,
26 | },
27 | badgeText: {
28 | color: 'white',
29 | fontSize: 12
30 | },
31 | rightContainer: {
32 | flex: 1,
33 | justifyContent: 'center',
34 | },
35 | row: {
36 | flexDirection: 'row',
37 | justifyContent: 'space-between',
38 | },
39 | name: {
40 | fontWeight: 'bold',
41 | fontSize: 18,
42 | marginBottom: 3,
43 | },
44 | text: {
45 | color: 'grey',
46 | }
47 | });
48 |
49 | export default styles;
--------------------------------------------------------------------------------
/components/ChatRoomItem/styles.ts:
--------------------------------------------------------------------------------
1 | import { StyleSheet } from 'react-native';
2 |
3 | const styles = StyleSheet.create({
4 | container: {
5 | flexDirection: 'row',
6 | padding: 10,
7 | },
8 | image: {
9 | height: 50,
10 | width: 50,
11 | borderRadius: 30,
12 | marginRight: 10,
13 | },
14 | badgeContainer: {
15 | backgroundColor: '#3777f0',
16 | width: 20,
17 | height: 20,
18 | borderRadius: 10,
19 | borderWidth: 1,
20 | borderColor: 'white',
21 | justifyContent: 'center',
22 | alignItems: 'center',
23 | position: 'absolute',
24 | left: 45,
25 | top: 10,
26 | },
27 | badgeText: {
28 | color: 'white',
29 | fontSize: 12
30 | },
31 | rightContainer: {
32 | flex: 1,
33 | justifyContent: 'center',
34 | },
35 | row: {
36 | flexDirection: 'row',
37 | justifyContent: 'space-between',
38 | },
39 | name: {
40 | fontWeight: 'bold',
41 | fontSize: 18,
42 | marginBottom: 3,
43 | },
44 | text: {
45 | color: 'grey',
46 | }
47 | });
48 |
49 | export default styles;
--------------------------------------------------------------------------------
/components/UserItem/UserItem.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Text, Image, View, Pressable } from "react-native";
3 | import { useNavigation } from "@react-navigation/core";
4 | import styles from "./styles";
5 | import { Feather } from "@expo/vector-icons";
6 |
7 | export default function UserItem({
8 | user,
9 | onPress,
10 | onLongPress,
11 | isSelected,
12 | isAdmin = false,
13 | }) {
14 | // null | false | true
15 | return (
16 |
21 |
22 |
23 |
24 | {user.name}
25 | {isAdmin && admin}
26 |
27 |
28 | {isSelected !== undefined && (
29 |
34 | )}
35 |
36 | );
37 | }
38 |
--------------------------------------------------------------------------------
/hooks/useCachedResources.ts:
--------------------------------------------------------------------------------
1 | import { Ionicons } from '@expo/vector-icons';
2 | import * as Font from 'expo-font';
3 | import * as SplashScreen from 'expo-splash-screen';
4 | import * as React from 'react';
5 |
6 | export default function useCachedResources() {
7 | const [isLoadingComplete, setLoadingComplete] = React.useState(false);
8 |
9 | // Load any resources or data that we need prior to rendering the app
10 | React.useEffect(() => {
11 | async function loadResourcesAndDataAsync() {
12 | try {
13 | SplashScreen.preventAutoHideAsync();
14 |
15 | // Load fonts
16 | await Font.loadAsync({
17 | ...Ionicons.font,
18 | 'space-mono': require('../assets/fonts/SpaceMono-Regular.ttf'),
19 | });
20 | } catch (e) {
21 | // We might want to provide this error information to an error reporting service
22 | console.warn(e);
23 | } finally {
24 | setLoadingComplete(true);
25 | SplashScreen.hideAsync();
26 | }
27 | }
28 |
29 | loadResourcesAndDataAsync();
30 | }, []);
31 |
32 | return isLoadingComplete;
33 | }
34 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Vadim Savin
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/screens/NotFoundScreen.tsx:
--------------------------------------------------------------------------------
1 | import { StackScreenProps } from '@react-navigation/stack';
2 | import * as React from 'react';
3 | import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
4 |
5 | import { RootStackParamList } from '../types';
6 |
7 | export default function NotFoundScreen({
8 | navigation,
9 | }: StackScreenProps) {
10 | return (
11 |
12 | This screen doesn't exist.
13 | navigation.replace('Root')} style={styles.link}>
14 | Go to home screen!
15 |
16 |
17 | );
18 | }
19 |
20 | const styles = StyleSheet.create({
21 | container: {
22 | flex: 1,
23 | backgroundColor: '#fff',
24 | alignItems: 'center',
25 | justifyContent: 'center',
26 | padding: 20,
27 | },
28 | title: {
29 | fontSize: 20,
30 | fontWeight: 'bold',
31 | },
32 | link: {
33 | marginTop: 15,
34 | paddingVertical: 15,
35 | },
36 | linkText: {
37 | fontSize: 14,
38 | color: '#2e78b7',
39 | },
40 | });
41 |
--------------------------------------------------------------------------------
/amplify/backend/function/SignalClonePostConfirmation/src/custom.js:
--------------------------------------------------------------------------------
1 | const aws = require("aws-sdk");
2 | const ddb = new aws.DynamoDB();
3 |
4 | const tableName = process.env.USERTABLE;
5 |
6 | exports.handler = async (event) => {
7 | // event event.request.userAttributes.(sub, email)
8 | // insert code to be executed by your lambda trigger
9 |
10 | if (!event?.request?.userAttributes?.sub){
11 | console.log("No sub provided");
12 | return;
13 | }
14 |
15 | const now = new Date();
16 | const timestamp = now.getTime();
17 |
18 | const userItem = {
19 | __typename: { S: 'User' },
20 | _lastChangedAt: { N: timestamp.toString() },
21 | _version: { N: "1" },
22 | createdAt: { S: now.toISOString() },
23 | updatedAt: { S: now.toISOString() },
24 | id: { S: event.request.userAttributes.sub },
25 | name: { S: event.request.userAttributes.email },
26 | }
27 |
28 | const params = {
29 | Item: userItem,
30 | TableName: tableName
31 | };
32 |
33 | // save a new user to DynamoDB
34 | try {
35 | await ddb.putItem(params).promise();
36 | console.log("success");
37 | } catch (e) {
38 | console.log(e)
39 | }
40 | };
41 |
--------------------------------------------------------------------------------
/amplify/team-provider-info.json:
--------------------------------------------------------------------------------
1 | {
2 | "staging": {
3 | "awscloudformation": {
4 | "AuthRoleName": "amplify-signalclone-staging-142023-authRole",
5 | "UnauthRoleArn": "arn:aws:iam::704219588443:role/amplify-signalclone-staging-142023-unauthRole",
6 | "AuthRoleArn": "arn:aws:iam::704219588443:role/amplify-signalclone-staging-142023-authRole",
7 | "Region": "eu-west-1",
8 | "DeploymentBucketName": "amplify-signalclone-staging-142023-deployment",
9 | "UnauthRoleName": "amplify-signalclone-staging-142023-unauthRole",
10 | "StackName": "amplify-signalclone-staging-142023",
11 | "StackId": "arn:aws:cloudformation:eu-west-1:704219588443:stack/amplify-signalclone-staging-142023/c22c1eb0-01c1-11ec-a710-0a765228daef",
12 | "AmplifyAppId": "d28kwkid6yb4zl"
13 | },
14 | "categories": {
15 | "auth": {
16 | "SignalClone": {}
17 | },
18 | "function": {
19 | "SignalClonePostConfirmation": {
20 | "deploymentBucketName": "amplify-signalclone-staging-142023-deployment",
21 | "s3Key": "amplify-builds/SignalClonePostConfirmation-6f504a5254326c567a71-build.zip"
22 | }
23 | }
24 | }
25 | }
26 | }
--------------------------------------------------------------------------------
/amplify/backend/function/SignalClonePostConfirmation/src/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @fileoverview
3 | *
4 | * This CloudFormation Trigger creates a handler which awaits the other handlers
5 | * specified in the `MODULES` env var, located at `./${MODULE}`.
6 | */
7 |
8 | /**
9 | * The names of modules to load are stored as a comma-delimited string in the
10 | * `MODULES` env var.
11 | */
12 | const moduleNames = process.env.MODULES.split(',');
13 | /**
14 | * The array of imported modules.
15 | */
16 | const modules = moduleNames.map(name => require(`./${name}`));
17 |
18 | /**
19 | * This async handler iterates over the given modules and awaits them.
20 | *
21 | * @see https://docs.aws.amazon.com/lambda/latest/dg/nodejs-handler.html#nodejs-handler-async
22 | *
23 | * @param {object} event
24 | *
25 | * The event that triggered this Lambda.
26 | *
27 | * @returns
28 | *
29 | * The handler response.
30 | */
31 | exports.handler = async event => {
32 | /**
33 | * Instead of naively iterating over all handlers, run them concurrently with
34 | * `await Promise.all(...)`. This would otherwise just be determined by the
35 | * order of names in the `MODULES` var.
36 | */
37 | await Promise.all(modules.map(module => module.handler(event)));
38 | return event;
39 | };
40 |
--------------------------------------------------------------------------------
/screens/HomeScreen.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 |
3 | import { View, StyleSheet, FlatList } from "react-native";
4 | import { Auth, DataStore } from "aws-amplify";
5 | import { ChatRoom, ChatRoomUser } from "../src/models";
6 | import ChatRoomItem from "../components/ChatRoomItem";
7 |
8 | export default function TabOneScreen() {
9 | const [chatRooms, setChatRooms] = useState([]);
10 |
11 | useEffect(() => {
12 | const fetchChatRooms = async () => {
13 | const userData = await Auth.currentAuthenticatedUser();
14 |
15 | const chatRooms = (await DataStore.query(ChatRoomUser))
16 | .filter(
17 | (chatRoomUser) => chatRoomUser.user.id === userData.attributes.sub
18 | )
19 | .map((chatRoomUser) => chatRoomUser.chatroom);
20 |
21 | setChatRooms(chatRooms);
22 | };
23 | fetchChatRooms();
24 | }, []);
25 |
26 |
27 |
28 | return (
29 |
30 | }
33 | showsVerticalScrollIndicator={false}
34 | />
35 |
36 | );
37 | }
38 |
39 | const styles = StyleSheet.create({
40 | page: {
41 | backgroundColor: "white",
42 | flex: 1,
43 | },
44 | });
45 |
--------------------------------------------------------------------------------
/amplify/cli.json:
--------------------------------------------------------------------------------
1 | {
2 | "features": {
3 | "graphqltransformer": {
4 | "addmissingownerfields": true,
5 | "improvepluralization": true,
6 | "validatetypenamereservedwords": true,
7 | "useexperimentalpipelinedtransformer": false,
8 | "enableiterativegsiupdates": true,
9 | "secondarykeyasgsi": true,
10 | "skipoverridemutationinputtypes": true
11 | },
12 | "frontend-ios": {
13 | "enablexcodeintegration": true
14 | },
15 | "auth": {
16 | "enablecaseinsensitivity": true,
17 | "useinclusiveterminology": true,
18 | "breakcirculardependency": true
19 | },
20 | "codegen": {
21 | "useappsyncmodelgenplugin": true,
22 | "usedocsgeneratorplugin": true,
23 | "usetypesgeneratorplugin": true,
24 | "cleangeneratedmodelsdirectory": true,
25 | "retaincasestyle": true,
26 | "addtimestampfields": true,
27 | "handlelistnullabilitytransparently": true,
28 | "emitauthprovider": true,
29 | "generateindexrules": true,
30 | "enabledartnullsafety": true
31 | },
32 | "appsync": {
33 | "generategraphqlpermissions": true
34 | }
35 | }
36 | }
--------------------------------------------------------------------------------
/amplify/backend/storage/s39381675c/parameters.json:
--------------------------------------------------------------------------------
1 | {
2 | "bucketName": "signalcloneeb35d96961524b3f953ede84b4f5c284",
3 | "authPolicyName": "s3_amplify_9381675c",
4 | "unauthPolicyName": "s3_amplify_9381675c",
5 | "authRoleName": {
6 | "Ref": "AuthRoleName"
7 | },
8 | "unauthRoleName": {
9 | "Ref": "UnauthRoleName"
10 | },
11 | "selectedGuestPermissions": [
12 | "s3:GetObject",
13 | "s3:ListBucket"
14 | ],
15 | "selectedAuthenticatedPermissions": [
16 | "s3:PutObject",
17 | "s3:GetObject",
18 | "s3:ListBucket"
19 | ],
20 | "s3PermissionsAuthenticatedPublic": "s3:PutObject,s3:GetObject",
21 | "s3PublicPolicy": "Public_policy_97ebde3a",
22 | "s3PermissionsAuthenticatedUploads": "s3:PutObject",
23 | "s3UploadsPolicy": "Uploads_policy_97ebde3a",
24 | "s3PermissionsAuthenticatedProtected": "s3:PutObject,s3:GetObject",
25 | "s3ProtectedPolicy": "Protected_policy_97ebde3a",
26 | "s3PermissionsAuthenticatedPrivate": "s3:PutObject,s3:GetObject",
27 | "s3PrivatePolicy": "Private_policy_97ebde3a",
28 | "AuthenticatedAllowList": "ALLOW",
29 | "s3ReadPolicy": "read_policy_97ebde3a",
30 | "s3PermissionsGuestPublic": "DISALLOW",
31 | "s3PermissionsGuestUploads": "DISALLOW",
32 | "GuestAllowList": "DISALLOW",
33 | "triggerFunction": "NONE"
34 | }
--------------------------------------------------------------------------------
/assets/dummy-data/Users.ts:
--------------------------------------------------------------------------------
1 | export default [{
2 | id: 'u1',
3 | name: 'Vadim',
4 | imageUri: 'https://notjustdev-dummy.s3.us-east-2.amazonaws.com/avatars/vadim.jpg',
5 | status: "Hello there, how are you"
6 | }, {
7 | id: 'u2',
8 | name: 'Elon Musk',
9 | imageUri: 'https://notjustdev-dummy.s3.us-east-2.amazonaws.com/avatars/elon.png',
10 | }, {
11 | id: 'u3',
12 | name: 'Jeff',
13 | imageUri: 'https://notjustdev-dummy.s3.us-east-2.amazonaws.com/avatars/jeff.jpeg',
14 | }, {
15 | id: 'u4',
16 | name: 'Zuck',
17 | imageUri: 'https://notjustdev-dummy.s3.us-east-2.amazonaws.com/avatars/zuck.jpeg',
18 | }, {
19 | id: 'u5',
20 | name: 'Graham',
21 | imageUri: 'https://notjustdev-dummy.s3.us-east-2.amazonaws.com/avatars/graham.jpg',
22 | }, {
23 | id: 'u6',
24 | name: 'Biahaze',
25 | imageUri: 'https://notjustdev-dummy.s3.us-east-2.amazonaws.com/avatars/biahaze.jpg',
26 | }, {
27 | id: 'u7',
28 | name: 'Sus?',
29 | imageUri: 'https://notjustdev-dummy.s3.us-east-2.amazonaws.com/avatars/1.jpg',
30 | }, {
31 | id: 'u8',
32 | name: 'Daniel',
33 | imageUri: 'https://notjustdev-dummy.s3.us-east-2.amazonaws.com/avatars/2.jpg',
34 | }, {
35 | id: 'u9',
36 | name: 'Carlos',
37 | imageUri: 'https://notjustdev-dummy.s3.us-east-2.amazonaws.com/avatars/3.jpg',
38 | }, {
39 | id: 'u10',
40 | name: 'Angelina Jolie',
41 | imageUri: 'https://notjustdev-dummy.s3.us-east-2.amazonaws.com/avatars/4.jpg',
42 | }]
43 |
--------------------------------------------------------------------------------
/components/Themed.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Learn more about Light and Dark modes:
3 | * https://docs.expo.io/guides/color-schemes/
4 | */
5 |
6 | import * as React from 'react';
7 | import { Text as DefaultText, View as DefaultView } from 'react-native';
8 |
9 | import Colors from '../constants/Colors';
10 | import useColorScheme from '../hooks/useColorScheme';
11 |
12 | export function useThemeColor(
13 | props: { light?: string; dark?: string },
14 | colorName: keyof typeof Colors.light & keyof typeof Colors.dark
15 | ) {
16 | const theme = useColorScheme();
17 | const colorFromProps = props[theme];
18 |
19 | if (colorFromProps) {
20 | return colorFromProps;
21 | } else {
22 | return Colors[theme][colorName];
23 | }
24 | }
25 |
26 | type ThemeProps = {
27 | lightColor?: string;
28 | darkColor?: string;
29 | };
30 |
31 | export type TextProps = ThemeProps & DefaultText['props'];
32 | export type ViewProps = ThemeProps & DefaultView['props'];
33 |
34 | export function Text(props: TextProps) {
35 | const { style, lightColor, darkColor, ...otherProps } = props;
36 | const color = useThemeColor({ light: lightColor, dark: darkColor }, 'text');
37 |
38 | return ;
39 | }
40 |
41 | export function View(props: ViewProps) {
42 | const { style, lightColor, darkColor, ...otherProps } = props;
43 | const backgroundColor = useThemeColor({ light: lightColor, dark: darkColor }, 'background');
44 |
45 | return ;
46 | }
47 |
--------------------------------------------------------------------------------
/amplify/backend/api/SignalClone/schema.graphql:
--------------------------------------------------------------------------------
1 | enum MessageStatus {
2 | SENT
3 | DELIVERED
4 | READ
5 | }
6 |
7 | type Message @model @auth(rules: [{allow: public}]) @key(name: "byUser", fields: ["userID"]) @key(name: "byChatRoom", fields: ["chatroomID"]) {
8 | id: ID!
9 | content: String
10 | userID: ID
11 | chatroomID: ID
12 | image: String
13 | audio: String
14 | status: MessageStatus
15 | replyToMessageID: ID
16 | forUserId: ID
17 | }
18 |
19 | type ChatRoom @model @auth(rules: [{allow: public}]) {
20 | id: ID!
21 | newMessages: Int
22 | LastMessage: Message @connection
23 | Messages: [Message] @connection(keyName: "byChatRoom", fields: ["id"])
24 | ChatRoomUsers: [ChatRoomUser] @connection(keyName: "byChatRoom", fields: ["id"])
25 | Admin: User @connection
26 | name: String
27 | imageUri: String
28 | }
29 |
30 | type User @model @auth(rules: [{allow: public}]) {
31 | id: ID!
32 | name: String!
33 | imageUri: String
34 | status: String
35 | Messages: [Message] @connection(keyName: "byUser", fields: ["id"])
36 | chatrooms: [ChatRoomUser] @connection(keyName: "byUser", fields: ["id"])
37 | lastOnlineAt: AWSTimestamp
38 | publicKey: String
39 | }
40 |
41 | type ChatRoomUser @model(queries: null) @key(name: "byChatRoom", fields: ["chatroomID", "userID"]) @key(name: "byUser", fields: ["userID", "chatroomID"]) @auth(rules: [{allow: public}, {allow: public}]) {
42 | id: ID!
43 | chatroomID: ID!
44 | userID: ID!
45 | chatroom: ChatRoom! @connection(fields: ["chatroomID"])
46 | user: User! @connection(fields: ["userID"])
47 | }
48 |
--------------------------------------------------------------------------------
/assets/dummy-data/Chats.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | id: '1',
3 | users: [{
4 | id: 'u1',
5 | name: 'Vadim',
6 | imageUri: 'https://notjustdev-dummy.s3.us-east-2.amazonaws.com/avatars/vadim.jpg',
7 | }, {
8 | id: 'u2',
9 | name: 'Elon Musk',
10 | imageUri: 'https://notjustdev-dummy.s3.us-east-2.amazonaws.com/avatars/elon.png',
11 | }],
12 | messages: [{
13 | id: 'm1',
14 | content: 'How are you, Elon!',
15 | createdAt: '2020-10-10T12:48:00.000Z',
16 | user: {
17 | id: 'u1',
18 | name: 'Vadim',
19 | },
20 | }, {
21 | id: 'm2',
22 | content: 'I am good, good',
23 | createdAt: '2020-10-03T14:49:00.000Z',
24 | user: {
25 | id: 'u2',
26 | name: 'Elon Musk',
27 | },
28 | }, {
29 | id: 'm3',
30 | content: 'What about you?',
31 | createdAt: '2020-10-03T14:49:40.000Z',
32 | user: {
33 | id: 'u2',
34 | name: 'Elon Musk',
35 | },
36 | }, {
37 | id: 'm4',
38 | content: 'Good as well, preparing for the stream now.',
39 | createdAt: '2020-10-03T14:50:00.000Z',
40 | user: {
41 | id: 'u1',
42 | name: 'Vadim',
43 | },
44 | }, {
45 | id: 'm5',
46 | content: 'How is SpaceX doing?',
47 | createdAt: '2020-10-03T14:51:00.000Z',
48 | user: {
49 | id: 'u1',
50 | name: 'Vadim',
51 | },
52 | }, {
53 | id: 'm6',
54 | content: 'going to the Moooooon',
55 | createdAt: '2020-10-03T14:49:00.000Z',
56 | user: {
57 | id: 'u2',
58 | name: 'Elon Musk',
59 | },
60 | }, {
61 | id: 'm7',
62 | content: 'btw, SpaceX is interested in buying notJust.dev!',
63 | createdAt: '2020-10-03T14:53:00.000Z',
64 | user: {
65 | id: 'u2',
66 | name: 'Elon Musk',
67 | },
68 | }]
69 | }
70 |
71 |
--------------------------------------------------------------------------------
/amplify/backend/api/SignalClone/stacks/CustomResources.json:
--------------------------------------------------------------------------------
1 | {
2 | "AWSTemplateFormatVersion": "2010-09-09",
3 | "Description": "An auto-generated nested stack.",
4 | "Metadata": {},
5 | "Parameters": {
6 | "AppSyncApiId": {
7 | "Type": "String",
8 | "Description": "The id of the AppSync API associated with this project."
9 | },
10 | "AppSyncApiName": {
11 | "Type": "String",
12 | "Description": "The name of the AppSync API",
13 | "Default": "AppSyncSimpleTransform"
14 | },
15 | "env": {
16 | "Type": "String",
17 | "Description": "The environment name. e.g. Dev, Test, or Production",
18 | "Default": "NONE"
19 | },
20 | "S3DeploymentBucket": {
21 | "Type": "String",
22 | "Description": "The S3 bucket containing all deployment assets for the project."
23 | },
24 | "S3DeploymentRootKey": {
25 | "Type": "String",
26 | "Description": "An S3 key relative to the S3DeploymentBucket that points to the root\nof the deployment directory."
27 | }
28 | },
29 | "Resources": {
30 | "EmptyResource": {
31 | "Type": "Custom::EmptyResource",
32 | "Condition": "AlwaysFalse"
33 | }
34 | },
35 | "Conditions": {
36 | "HasEnvironmentParameter": {
37 | "Fn::Not": [
38 | {
39 | "Fn::Equals": [
40 | {
41 | "Ref": "env"
42 | },
43 | "NONE"
44 | ]
45 | }
46 | ]
47 | },
48 | "AlwaysFalse": {
49 | "Fn::Equals": ["true", "false"]
50 | }
51 | },
52 | "Outputs": {
53 | "EmptyOutput": {
54 | "Description": "An empty output. You may delete this if you have at least one resource above.",
55 | "Value": ""
56 | }
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/amplify/backend/backend-config.json:
--------------------------------------------------------------------------------
1 | {
2 | "auth": {
3 | "SignalClone": {
4 | "service": "Cognito",
5 | "providerPlugin": "awscloudformation",
6 | "dependsOn": [
7 | {
8 | "category": "function",
9 | "resourceName": "SignalClonePostConfirmation",
10 | "triggerProvider": "Cognito",
11 | "attributes": [
12 | "Arn",
13 | "Name"
14 | ]
15 | }
16 | ],
17 | "customAuth": false,
18 | "frontendAuthConfig": {
19 | "loginMechanism": [],
20 | "signupAttributes": [
21 | "EMAIL"
22 | ],
23 | "passwordProtectionSettings": {
24 | "passwordPolicyMinLength": 8,
25 | "passwordPolicyCharacters": []
26 | },
27 | "mfaConfiguration": "OFF",
28 | "mfaTypes": [
29 | "SMS"
30 | ]
31 | }
32 | }
33 | },
34 | "api": {
35 | "SignalClone": {
36 | "service": "AppSync",
37 | "providerPlugin": "awscloudformation",
38 | "output": {
39 | "authConfig": {
40 | "defaultAuthentication": {
41 | "authenticationType": "API_KEY",
42 | "apiKeyConfig": {
43 | "apiKeyExpirationDays": 30,
44 | "description": "api key description"
45 | }
46 | },
47 | "additionalAuthenticationProviders": [
48 | {
49 | "authenticationType": "AWS_IAM"
50 | },
51 | {
52 | "authenticationType": "AMAZON_COGNITO_USER_POOLS",
53 | "userPoolConfig": {
54 | "userPoolId": "authSignalClone"
55 | }
56 | }
57 | ]
58 | }
59 | }
60 | }
61 | },
62 | "function": {
63 | "SignalClonePostConfirmation": {
64 | "build": true,
65 | "providerPlugin": "awscloudformation",
66 | "service": "Lambda"
67 | }
68 | },
69 | "storage": {
70 | "s39381675c": {
71 | "service": "S3",
72 | "providerPlugin": "awscloudformation"
73 | }
74 | }
75 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "main": "node_modules/expo/AppEntry.js",
3 | "scripts": {
4 | "start": "expo start",
5 | "android": "expo start --android",
6 | "ios": "expo start --ios",
7 | "web": "expo start --web",
8 | "eject": "expo eject",
9 | "test": "jest --watchAll"
10 | },
11 | "jest": {
12 | "preset": "jest-expo"
13 | },
14 | "dependencies": {
15 | "@expo/react-native-action-sheet": "^3.11.0",
16 | "@expo/vector-icons": "^12.0.0",
17 | "@react-native-async-storage/async-storage": "~1.15.0",
18 | "@react-native-community/masked-view": "0.1.10",
19 | "@react-native-community/netinfo": "^6.0.0",
20 | "@react-navigation/bottom-tabs": "5.11.2",
21 | "@react-navigation/native": "~5.8.10",
22 | "@react-navigation/stack": "~5.12.8",
23 | "@stablelib/base64": "^1.0.1",
24 | "@stablelib/utf8": "^1.0.1",
25 | "aws-amplify": "^4.2.4",
26 | "aws-amplify-react-native": "^5.0.3",
27 | "expo": "~42.0.1",
28 | "expo-asset": "~8.3.2",
29 | "expo-av": "~9.2.3",
30 | "expo-constants": "~11.0.1",
31 | "expo-font": "~9.2.1",
32 | "expo-image-picker": "~10.2.2",
33 | "expo-linking": "~2.3.1",
34 | "expo-random": "~11.2.0",
35 | "expo-splash-screen": "~0.11.2",
36 | "expo-status-bar": "~1.0.4",
37 | "expo-web-browser": "~9.2.0",
38 | "moment": "^2.29.1",
39 | "react": "16.13.1",
40 | "react-dom": "16.13.1",
41 | "react-native": "https://github.com/expo/react-native/archive/sdk-42.0.0.tar.gz",
42 | "react-native-emoji-selector": "^0.2.0",
43 | "react-native-gesture-handler": "~1.10.2",
44 | "react-native-reanimated": "~2.2.0",
45 | "react-native-safe-area-context": "3.2.0",
46 | "react-native-screens": "~3.4.0",
47 | "react-native-web": "~0.13.12",
48 | "tweetnacl": "^0.14.5",
49 | "uuid": "^3.4.0"
50 | },
51 | "devDependencies": {
52 | "@babel/core": "^7.9.0",
53 | "@expo/metro-config": "^0.1.84",
54 | "@types/react": "~16.9.35",
55 | "@types/react-native": "~0.63.2",
56 | "jest-expo": "~41.0.0-beta.0",
57 | "typescript": "~4.0.0"
58 | },
59 | "private": true
60 | }
61 |
--------------------------------------------------------------------------------
/screens/Settings.tsx:
--------------------------------------------------------------------------------
1 | import { Auth, DataStore } from "aws-amplify";
2 | import React from "react";
3 | import { View, Text, Pressable, Alert } from "react-native";
4 | import { generateKeyPair } from "../utils/crypto";
5 | import AsyncStorage from "@react-native-async-storage/async-storage";
6 | import { User as UserModel } from "../src/models";
7 |
8 | export const PRIVATE_KEY = "PRIVATE_KEY";
9 |
10 | const Settings = () => {
11 | const logOut = async () => {
12 | await DataStore.clear();
13 | Auth.signOut();
14 | };
15 |
16 | const updateKeyPair = async () => {
17 | // generate private/public key
18 | const { publicKey, secretKey } = generateKeyPair();
19 | console.log(publicKey, secretKey);
20 |
21 | // save private key to Async storage
22 | await AsyncStorage.setItem(PRIVATE_KEY, secretKey.toString());
23 | console.log("secret key was saved");
24 |
25 | // save public key to UserModel in Datastore
26 | const userData = await Auth.currentAuthenticatedUser();
27 | const dbUser = await DataStore.query(UserModel, userData.attributes.sub);
28 |
29 | if (!dbUser) {
30 | Alert.alert("User not found!");
31 | return;
32 | }
33 |
34 | await DataStore.save(
35 | UserModel.copyOf(dbUser, (updated) => {
36 | updated.publicKey = publicKey.toString();
37 | })
38 | );
39 |
40 | console.log(dbUser);
41 |
42 | Alert.alert("Successfully updated the keypair.");
43 | };
44 |
45 | return (
46 |
47 | Setting
48 |
49 |
59 | Update keypair
60 |
61 |
62 |
72 | Logout
73 |
74 |
75 | );
76 | };
77 |
78 | export default Settings;
79 |
--------------------------------------------------------------------------------
/components/EditScreenInfo.tsx:
--------------------------------------------------------------------------------
1 | import * as WebBrowser from 'expo-web-browser';
2 | import React from 'react';
3 | import { StyleSheet, TouchableOpacity } from 'react-native';
4 |
5 | import Colors from '../constants/Colors';
6 | import { MonoText } from './StyledText';
7 | import { Text, View } from './Themed';
8 |
9 | export default function EditScreenInfo({ path }: { path: string }) {
10 | return (
11 |
12 |
13 |
17 | Open up the code for this screen:
18 |
19 |
20 |
24 | {path}
25 |
26 |
27 |
31 | Change any of the text, save the file, and your app will automatically update.
32 |
33 |
34 |
35 |
36 |
37 |
38 | Tap here if your app doesn't automatically update after making changes
39 |
40 |
41 |
42 |
43 | );
44 | }
45 |
46 | function handleHelpPress() {
47 | WebBrowser.openBrowserAsync(
48 | 'https://docs.expo.io/get-started/create-a-new-app/#opening-the-app-on-your-phonetablet'
49 | );
50 | }
51 |
52 | const styles = StyleSheet.create({
53 | getStartedContainer: {
54 | alignItems: 'center',
55 | marginHorizontal: 50,
56 | },
57 | homeScreenFilename: {
58 | marginVertical: 7,
59 | },
60 | codeHighlightContainer: {
61 | borderRadius: 3,
62 | paddingHorizontal: 4,
63 | },
64 | getStartedText: {
65 | fontSize: 17,
66 | lineHeight: 24,
67 | textAlign: 'center',
68 | },
69 | helpContainer: {
70 | marginTop: 15,
71 | marginHorizontal: 20,
72 | alignItems: 'center',
73 | },
74 | helpLink: {
75 | paddingVertical: 15,
76 | },
77 | helpLinkText: {
78 | textAlign: 'center',
79 | },
80 | });
81 |
--------------------------------------------------------------------------------
/utils/crypto.js:
--------------------------------------------------------------------------------
1 | import { getRandomBytes } from "expo-random";
2 | import { box, setPRNG } from "tweetnacl";
3 | import { decode as decodeUTF8, encode as encodeUTF8 } from "@stablelib/utf8";
4 | import {
5 | decode as decodeBase64,
6 | encode as encodeBase64,
7 | } from "@stablelib/base64";
8 | import AsyncStorage from "@react-native-async-storage/async-storage";
9 |
10 | export const PRIVATE_KEY = "PRIVATE_KEY";
11 |
12 | setPRNG((x, n) => {
13 | const randomBytes = getRandomBytes(n);
14 | for (let i = 0; i < n; i++) {
15 | x[i] = randomBytes[i];
16 | }
17 | });
18 |
19 | const newNonce = () => getRandomBytes(box.nonceLength);
20 | export const generateKeyPair = () => box.keyPair();
21 |
22 | export const encrypt = (secretOrSharedKey, json, key) => {
23 | const nonce = newNonce();
24 | const messageUint8 = encodeUTF8(JSON.stringify(json));
25 | const encrypted = key
26 | ? box(messageUint8, nonce, key, secretOrSharedKey)
27 | : box.after(messageUint8, nonce, secretOrSharedKey);
28 |
29 | const fullMessage = new Uint8Array(nonce.length + encrypted.length);
30 | fullMessage.set(nonce);
31 | fullMessage.set(encrypted, nonce.length);
32 |
33 | const base64FullMessage = encodeBase64(fullMessage);
34 | return base64FullMessage;
35 | };
36 |
37 | export const decrypt = (secretOrSharedKey, messageWithNonce, key) => {
38 | const messageWithNonceAsUint8Array = decodeBase64(messageWithNonce);
39 | const nonce = messageWithNonceAsUint8Array.slice(0, box.nonceLength);
40 | const message = messageWithNonceAsUint8Array.slice(
41 | box.nonceLength,
42 | messageWithNonce.length
43 | );
44 |
45 | const decrypted = key
46 | ? box.open(message, nonce, key, secretOrSharedKey)
47 | : box.open.after(message, nonce, secretOrSharedKey);
48 |
49 | if (!decrypted) {
50 | throw new Error("Could not decrypt message");
51 | }
52 |
53 | const base64DecryptedMessage = decodeUTF8(decrypted);
54 | return JSON.parse(base64DecryptedMessage);
55 | };
56 |
57 | export const stringToUint8Array = (content) =>
58 | Uint8Array.from(content.split(",").map((str) => parseInt(str)));
59 |
60 | export const getMySecretKey = async () => {
61 | const keyString = await AsyncStorage.getItem(PRIVATE_KEY);
62 | if (!keyString) {
63 | Alert.alert(
64 | "You haven't set your keypair yet",
65 | "Go to settings, and generate a new keypair",
66 | [
67 | {
68 | text: "Open setting",
69 | onPress: () => navigation.navigate("Settings"),
70 | },
71 | ]
72 | );
73 | return;
74 | }
75 |
76 | return stringToUint8Array(keyString);
77 | };
78 |
--------------------------------------------------------------------------------
/amplify/backend/auth/SignalClone/parameters.json:
--------------------------------------------------------------------------------
1 | {
2 | "identityPoolName": "testAuthIdentityPool",
3 | "allowUnauthenticatedIdentities": false,
4 | "resourceNameTruncated": "signal29db2920",
5 | "userPoolName": "SignalClone",
6 | "autoVerifiedAttributes": [
7 | "email"
8 | ],
9 | "mfaConfiguration": "OFF",
10 | "mfaTypes": [
11 | "SMS Text Message"
12 | ],
13 | "smsAuthenticationMessage": "Your authentication code is {####}",
14 | "smsVerificationMessage": "Your verification code is {####}",
15 | "emailVerificationSubject": "Forgot password code: {####}",
16 | "emailVerificationMessage": "Forgot password code: {####}",
17 | "defaultPasswordPolicy": false,
18 | "passwordPolicyMinLength": 8,
19 | "passwordPolicyCharacters": [],
20 | "requiredAttributes": [
21 | "email"
22 | ],
23 | "aliasAttributes": [],
24 | "userpoolClientGenerateSecret": false,
25 | "userpoolClientRefreshTokenValidity": 30,
26 | "userpoolClientWriteAttributes": [],
27 | "userpoolClientReadAttributes": [],
28 | "userpoolClientLambdaRole": "Signal29db2920_userpoolclient_lambda_role",
29 | "userpoolClientSetAttributes": false,
30 | "sharedId": "29db2920",
31 | "resourceName": "SignalClone",
32 | "authSelections": "identityPoolAndUserPool",
33 | "authRoleArn": {
34 | "Fn::GetAtt": [
35 | "AuthRole",
36 | "Arn"
37 | ]
38 | },
39 | "unauthRoleArn": {
40 | "Fn::GetAtt": [
41 | "UnauthRole",
42 | "Arn"
43 | ]
44 | },
45 | "serviceName": "Cognito",
46 | "usernameAttributes": [
47 | "email"
48 | ],
49 | "useDefault": "manual",
50 | "userPoolGroups": false,
51 | "userPoolGroupList": [],
52 | "adminQueries": false,
53 | "thirdPartyAuth": false,
54 | "authProviders": [],
55 | "usernameCaseSensitive": false,
56 | "dependsOn": [
57 | {
58 | "category": "function",
59 | "resourceName": "SignalClonePostConfirmation",
60 | "triggerProvider": "Cognito",
61 | "attributes": [
62 | "Arn",
63 | "Name"
64 | ]
65 | }
66 | ],
67 | "triggers": "{\n \"PostConfirmation\": [\n \"custom\"\n ]\n}",
68 | "hostedUI": false,
69 | "parentStack": {
70 | "Ref": "AWS::StackId"
71 | },
72 | "authTriggerConnections": "[\n {\n \"triggerType\": \"PostConfirmation\",\n \"lambdaFunctionName\": \"SignalClonePostConfirmation\"\n }\n]",
73 | "breakCircularDependency": true,
74 | "permissions": []
75 | }
--------------------------------------------------------------------------------
/components/ChatRoomItem/ChatRoomItem.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import { Text, Image, View, Pressable, ActivityIndicator } from "react-native";
3 | import { useNavigation } from "@react-navigation/core";
4 | import { DataStore } from "@aws-amplify/datastore";
5 | import { ChatRoomUser, User, Message } from "../../src/models";
6 | import styles from "./styles";
7 | import Auth from "@aws-amplify/auth";
8 | import moment from "moment";
9 |
10 | export default function ChatRoomItem({ chatRoom }) {
11 | // const [users, setUsers] = useState([]); // all users in this chatroom
12 | const [user, setUser] = useState(null); // the display user
13 | const [lastMessage, setLastMessage] = useState();
14 | const [isLoading, setIsLoading] = useState(true);
15 |
16 | const navigation = useNavigation();
17 |
18 | useEffect(() => {
19 | const fetchUsers = async () => {
20 | const fetchedUsers = (await DataStore.query(ChatRoomUser))
21 | .filter((chatRoomUser) => chatRoomUser.chatroom.id === chatRoom.id)
22 | .map((chatRoomUser) => chatRoomUser.user);
23 |
24 | // setUsers(fetchedUsers);
25 |
26 | const authUser = await Auth.currentAuthenticatedUser();
27 | setUser(
28 | fetchedUsers.find((user) => user.id !== authUser.attributes.sub) || null
29 | );
30 | setIsLoading(false);
31 | };
32 | fetchUsers();
33 | }, []);
34 |
35 | useEffect(() => {
36 | if (!chatRoom.chatRoomLastMessageId) {
37 | return;
38 | }
39 | DataStore.query(Message, chatRoom.chatRoomLastMessageId).then(
40 | setLastMessage
41 | );
42 | }, []);
43 |
44 | const onPress = () => {
45 | navigation.navigate("ChatRoom", { id: chatRoom.id });
46 | };
47 |
48 | if (isLoading) {
49 | return ;
50 | }
51 |
52 | const time = moment(lastMessage?.createdAt).from(moment());
53 |
54 | return (
55 |
56 |
60 |
61 | {!!chatRoom.newMessages && (
62 |
63 | {chatRoom.newMessages}
64 |
65 | )}
66 |
67 |
68 |
69 | {chatRoom.name || user?.name}
70 | {time}
71 |
72 |
73 | {lastMessage?.content}
74 |
75 |
76 |
77 | );
78 | }
79 |
--------------------------------------------------------------------------------
/src/models/index.d.ts:
--------------------------------------------------------------------------------
1 | import { ModelInit, MutableModel, PersistentModelConstructor } from "@aws-amplify/datastore";
2 |
3 | export enum MessageStatus {
4 | SENT = "SENT",
5 | DELIVERED = "DELIVERED",
6 | READ = "READ"
7 | }
8 |
9 |
10 |
11 | type MessageMetaData = {
12 | readOnlyFields: 'createdAt' | 'updatedAt';
13 | }
14 |
15 | type ChatRoomMetaData = {
16 | readOnlyFields: 'createdAt' | 'updatedAt';
17 | }
18 |
19 | type ChatRoomUserMetaData = {
20 | readOnlyFields: 'createdAt' | 'updatedAt';
21 | }
22 |
23 | type UserMetaData = {
24 | readOnlyFields: 'createdAt' | 'updatedAt';
25 | }
26 |
27 | export declare class Message {
28 | readonly id: string;
29 | readonly content?: string;
30 | readonly userID?: string;
31 | readonly chatroomID?: string;
32 | readonly image?: string;
33 | readonly audio?: string;
34 | readonly status?: MessageStatus | keyof typeof MessageStatus;
35 | readonly replyToMessageID?: string;
36 | readonly forUserId?: string;
37 | readonly createdAt?: string;
38 | readonly updatedAt?: string;
39 | constructor(init: ModelInit);
40 | static copyOf(source: Message, mutator: (draft: MutableModel) => MutableModel | void): Message;
41 | }
42 |
43 | export declare class ChatRoom {
44 | readonly id: string;
45 | readonly newMessages?: number;
46 | readonly LastMessage?: Message;
47 | readonly Messages?: (Message | null)[];
48 | readonly ChatRoomUsers?: (ChatRoomUser | null)[];
49 | readonly Admin?: User;
50 | readonly name?: string;
51 | readonly imageUri?: string;
52 | readonly createdAt?: string;
53 | readonly updatedAt?: string;
54 | constructor(init: ModelInit);
55 | static copyOf(source: ChatRoom, mutator: (draft: MutableModel) => MutableModel | void): ChatRoom;
56 | }
57 |
58 | export declare class ChatRoomUser {
59 | readonly id: string;
60 | readonly chatroom: ChatRoom;
61 | readonly user: User;
62 | readonly createdAt?: string;
63 | readonly updatedAt?: string;
64 | constructor(init: ModelInit);
65 | static copyOf(source: ChatRoomUser, mutator: (draft: MutableModel) => MutableModel | void): ChatRoomUser;
66 | }
67 |
68 | export declare class User {
69 | readonly id: string;
70 | readonly name: string;
71 | readonly imageUri?: string;
72 | readonly status?: string;
73 | readonly Messages?: (Message | null)[];
74 | readonly chatrooms?: (ChatRoomUser | null)[];
75 | readonly lastOnlineAt?: number;
76 | readonly publicKey?: string;
77 | readonly createdAt?: string;
78 | readonly updatedAt?: string;
79 | constructor(init: ModelInit);
80 | static copyOf(source: User, mutator: (draft: MutableModel) => MutableModel | void): User;
81 | }
--------------------------------------------------------------------------------
/components/AudioPlayer/AudioPlayer.tsx:
--------------------------------------------------------------------------------
1 | import { Feather } from "@expo/vector-icons";
2 | import { Audio, AVPlaybackStatus } from "expo-av";
3 | import React, { useEffect, useState } from "react";
4 | import { View, Text, Pressable, StyleSheet } from "react-native";
5 |
6 | const AudioPlayer = ({ soundURI }) => {
7 | const [sound, setSound] = useState(null);
8 | const [paused, setPause] = useState(true);
9 | const [audioProgress, setAudioProgress] = useState(0);
10 | const [audioDuration, setAudioDuration] = useState(0);
11 |
12 | useEffect(() => {
13 | loadSound();
14 | () => {
15 | // unload sound
16 | if (sound) {
17 | sound.unloadAsync();
18 | }
19 | };
20 | }, [soundURI]);
21 |
22 | const loadSound = async () => {
23 | if (!soundURI) {
24 | return;
25 | }
26 |
27 | const { sound } = await Audio.Sound.createAsync(
28 | { uri: soundURI },
29 | {},
30 | onPlaybackStatusUpdate
31 | );
32 | setSound(sound);
33 | };
34 |
35 | // Audio
36 | const onPlaybackStatusUpdate = (status: AVPlaybackStatus) => {
37 | if (!status.isLoaded) {
38 | return;
39 | }
40 | setAudioProgress(status.positionMillis / (status.durationMillis || 1));
41 | setPause(!status.isPlaying);
42 | setAudioDuration(status.durationMillis || 0);
43 | };
44 |
45 | const playPauseSound = async () => {
46 | if (!sound) {
47 | return;
48 | }
49 | if (paused) {
50 | await sound.playFromPositionAsync(0);
51 | } else {
52 | await sound.pauseAsync();
53 | }
54 | };
55 |
56 | const getDuration = () => {
57 | const minutes = Math.floor(audioDuration / (60 * 1000));
58 | const seconds = Math.floor((audioDuration % (60 * 1000)) / 1000);
59 |
60 | return `${minutes}:${seconds < 10 ? "0" : ""}${seconds}`;
61 | };
62 |
63 | return (
64 |
65 |
66 |
67 |
68 |
69 |
70 |
73 |
74 |
75 | {getDuration()}
76 |
77 | );
78 | };
79 |
80 | const styles = StyleSheet.create({
81 | sendAudioContainer: {
82 | marginVertical: 10,
83 | padding: 10,
84 | flexDirection: "row",
85 | justifyContent: "space-between",
86 | alignItems: "center",
87 | alignSelf: "stretch",
88 | borderWidth: 1,
89 | borderColor: "lightgray",
90 | borderRadius: 10,
91 | backgroundColor: "white",
92 | },
93 |
94 | audiProgressBG: {
95 | height: 3,
96 | flex: 1,
97 | backgroundColor: "lightgray",
98 | borderRadius: 5,
99 | margin: 10,
100 | },
101 | audioProgressFG: {
102 | width: 10,
103 | height: 10,
104 | borderRadius: 10,
105 | backgroundColor: "#3777f0",
106 |
107 | position: "absolute",
108 | top: -3,
109 | },
110 | });
111 |
112 | export default AudioPlayer;
113 |
--------------------------------------------------------------------------------
/screens/ChatRoomScreen.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import {
3 | Text,
4 | View,
5 | StyleSheet,
6 | FlatList,
7 | SafeAreaView,
8 | ActivityIndicator,
9 | } from "react-native";
10 | import { useRoute, useNavigation } from "@react-navigation/core";
11 | import { DataStore } from "@aws-amplify/datastore";
12 | import { ChatRoom, Message as MessageModel } from "../src/models";
13 | import Message from "../components/Message";
14 | import MessageInput from "../components/MessageInput";
15 | import { Auth, SortDirection } from "aws-amplify";
16 |
17 | export default function ChatRoomScreen() {
18 | const [messages, setMessages] = useState([]);
19 | const [messageReplyTo, setMessageReplyTo] = useState(
20 | null
21 | );
22 | const [chatRoom, setChatRoom] = useState(null);
23 |
24 | const route = useRoute();
25 | const navigation = useNavigation();
26 |
27 | useEffect(() => {
28 | fetchChatRoom();
29 | }, []);
30 |
31 | useEffect(() => {
32 | fetchMessages();
33 | }, [chatRoom]);
34 |
35 | useEffect(() => {
36 | const subscription = DataStore.observe(MessageModel).subscribe((msg) => {
37 | // console.log(msg.model, msg.opType, msg.element);
38 | if (msg.model === MessageModel && msg.opType === "INSERT") {
39 | setMessages((existingMessage) => [msg.element, ...existingMessage]);
40 | }
41 | });
42 |
43 | return () => subscription.unsubscribe();
44 | }, []);
45 |
46 | const fetchChatRoom = async () => {
47 | if (!route.params?.id) {
48 | console.warn("No chatroom id provided");
49 | return;
50 | }
51 | const chatRoom = await DataStore.query(ChatRoom, route.params.id);
52 | if (!chatRoom) {
53 | console.error("Couldn't find a chat room with this id");
54 | } else {
55 | setChatRoom(chatRoom);
56 | }
57 | };
58 |
59 | const fetchMessages = async () => {
60 | if (!chatRoom) {
61 | return;
62 | }
63 | const authUser = await Auth.currentAuthenticatedUser();
64 | const myId = authUser.attributes.sub;
65 |
66 | const fetchedMessages = await DataStore.query(
67 | MessageModel,
68 | (message) => message.chatroomID("eq", chatRoom?.id).forUserId("eq", myId),
69 | {
70 | sort: (message) => message.createdAt(SortDirection.DESCENDING),
71 | }
72 | );
73 | // console.log(fetchedMessages);
74 | setMessages(fetchedMessages);
75 | };
76 |
77 | if (!chatRoom) {
78 | return ;
79 | }
80 |
81 | return (
82 |
83 | (
86 | setMessageReplyTo(item)}
89 | />
90 | )}
91 | inverted
92 | />
93 | setMessageReplyTo(null)}
97 | />
98 |
99 | );
100 | }
101 |
102 | const styles = StyleSheet.create({
103 | page: {
104 | backgroundColor: "white",
105 | flex: 1,
106 | },
107 | });
108 |
--------------------------------------------------------------------------------
/navigation/ChatRoomHeader.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import {
3 | View,
4 | Image,
5 | Text,
6 | useWindowDimensions,
7 | Pressable,
8 | } from "react-native";
9 | import { Feather } from "@expo/vector-icons";
10 | import { Auth, DataStore } from "aws-amplify";
11 | import { ChatRoom, ChatRoomUser, User } from "../src/models";
12 | import moment from "moment";
13 | import { useNavigation } from "@react-navigation/core";
14 |
15 | const ChatRoomHeader = ({ id, children }) => {
16 | const { width } = useWindowDimensions();
17 | const [user, setUser] = useState(null);
18 | const [allUsers, setAllUsers] = useState([]);
19 | const [chatRoom, setChatRoom] = useState(undefined);
20 |
21 | const navigation = useNavigation();
22 |
23 | const fetchUsers = async () => {
24 | const fetchedUsers = (await DataStore.query(ChatRoomUser))
25 | .filter((chatRoomUser) => chatRoomUser.chatroom.id === id)
26 | .map((chatRoomUser) => chatRoomUser.user);
27 |
28 | setAllUsers(fetchedUsers);
29 |
30 | const authUser = await Auth.currentAuthenticatedUser();
31 | setUser(
32 | fetchedUsers.find((user) => user.id !== authUser.attributes.sub) || null
33 | );
34 | };
35 |
36 | const fetchChatRoom = async () => {
37 | DataStore.query(ChatRoom, id).then(setChatRoom);
38 | };
39 |
40 | useEffect(() => {
41 | if (!id) {
42 | return;
43 | }
44 |
45 | fetchUsers();
46 | fetchChatRoom();
47 | }, []);
48 |
49 | const getLastOnlineText = () => {
50 | if (!user?.lastOnlineAt) {
51 | return null;
52 | }
53 |
54 | // if lastOnlineAt is less than 5 minutes ago, show him as ONLINE
55 | const lastOnlineDiffMS = moment().diff(moment(user.lastOnlineAt));
56 | if (lastOnlineDiffMS < 5 * 60 * 1000) {
57 | // less than 5 minutes
58 | return "online";
59 | } else {
60 | return `Last seen online ${moment(user.lastOnlineAt).fromNow()}`;
61 | }
62 | };
63 |
64 | const getUsernames = () => {
65 | return allUsers.map((user) => user.name).join(", ");
66 | };
67 |
68 | const openInfo = () => {
69 | // redirect to info page
70 | navigation.navigate("GroupInfoScreen", { id });
71 | };
72 |
73 | const isGroup = allUsers.length > 2;
74 |
75 | return (
76 |
86 |
92 |
93 |
94 |
95 | {chatRoom?.name || user?.name}
96 |
97 |
98 | {isGroup ? getUsernames() : getLastOnlineText()}
99 |
100 |
101 |
102 |
108 |
114 |
115 | );
116 | };
117 |
118 | export default ChatRoomHeader;
119 |
--------------------------------------------------------------------------------
/screens/GroupInfoScreen/GroupInfoScreen.tsx:
--------------------------------------------------------------------------------
1 | import { useRoute } from "@react-navigation/native";
2 | import { DataStore, Auth } from "aws-amplify";
3 | import React, { useEffect, useState } from "react";
4 | import { View, Text, StyleSheet, Alert } from "react-native";
5 | import { FlatList } from "react-native-gesture-handler";
6 | import UserItem from "../../components/UserItem";
7 | import { ChatRoom, User, ChatRoomUser } from "../../src/models";
8 |
9 | const GroupInfoScreen = () => {
10 | const [chatRoom, setChatRoom] = useState(null);
11 | const [allUsers, setAllUsers] = useState([]);
12 | const route = useRoute();
13 |
14 | useEffect(() => {
15 | fetchChatRoom();
16 | fetchUsers();
17 | }, []);
18 |
19 | const fetchChatRoom = async () => {
20 | if (!route.params?.id) {
21 | console.warn("No chatroom id provided");
22 | return;
23 | }
24 | const chatRoom = await DataStore.query(ChatRoom, route.params.id);
25 | if (!chatRoom) {
26 | console.error("Couldn't find a chat room with this id");
27 | } else {
28 | setChatRoom(chatRoom);
29 | }
30 | };
31 |
32 | const fetchUsers = async () => {
33 | const fetchedUsers = (await DataStore.query(ChatRoomUser))
34 | .filter((chatRoomUser) => chatRoomUser.chatroom.id === route.params?.id)
35 | .map((chatRoomUser) => chatRoomUser.user);
36 |
37 | setAllUsers(fetchedUsers);
38 | };
39 |
40 | const confirmDelete = async (user) => {
41 | // check if Auth user is admin of this group
42 | const authData = await Auth.currentAuthenticatedUser();
43 | if (chatRoom?.Admin?.id !== authData.attributes.sub) {
44 | Alert.alert("You are not the admin of this group");
45 | return;
46 | }
47 |
48 | if (user.id === chatRoom?.Admin?.id) {
49 | Alert.alert("You are the admin, you cannot delete yourself");
50 | return;
51 | }
52 |
53 | Alert.alert(
54 | "Confirm delete",
55 | `Are you sure you want to delete ${user.name} from the group`,
56 | [
57 | {
58 | text: "Delete",
59 | onPress: () => deleteUser(user),
60 | style: "destructive",
61 | },
62 | {
63 | text: "Cancel",
64 | },
65 | ]
66 | );
67 | };
68 |
69 | const deleteUser = async (user) => {
70 | const chatRoomUsersToDelete = await (
71 | await DataStore.query(ChatRoomUser)
72 | ).filter(
73 | (cru) => cru.chatroom.id === chatRoom.id && cru.user.id === user.id
74 | );
75 |
76 | console.log(chatRoomUsersToDelete);
77 |
78 | if (chatRoomUsersToDelete.length > 0) {
79 | await DataStore.delete(chatRoomUsersToDelete[0]);
80 |
81 | setAllUsers(allUsers.filter((u) => u.id !== user.id));
82 | }
83 | };
84 |
85 | return (
86 |
87 | {chatRoom?.name}
88 |
89 | Users ({allUsers.length})
90 | (
93 | confirmDelete(item)}
97 | />
98 | )}
99 | />
100 |
101 | );
102 | };
103 |
104 | const styles = StyleSheet.create({
105 | root: {
106 | backgroundColor: "white",
107 | padding: 10,
108 | flex: 1,
109 | },
110 | title: {
111 | fontSize: 18,
112 | fontWeight: "bold",
113 | paddingVertical: 10,
114 | },
115 | });
116 |
117 | export default GroupInfoScreen;
118 |
--------------------------------------------------------------------------------
/App.tsx:
--------------------------------------------------------------------------------
1 | import "react-native-gesture-handler";
2 | import { StatusBar } from "expo-status-bar";
3 | import React, { useEffect, useState } from "react";
4 | import { SafeAreaProvider } from "react-native-safe-area-context";
5 | import Amplify, { DataStore, Hub, Auth } from "aws-amplify";
6 | import { withAuthenticator } from "aws-amplify-react-native";
7 | import config from "./src/aws-exports";
8 | import { Message, User } from "./src/models";
9 | import { ActionSheetProvider } from "@expo/react-native-action-sheet";
10 |
11 | import useCachedResources from "./hooks/useCachedResources";
12 | import useColorScheme from "./hooks/useColorScheme";
13 | import Navigation from "./navigation";
14 | import moment from "moment";
15 |
16 | import { box } from "tweetnacl";
17 | import { generateKeyPair, encrypt, decrypt } from "./utils/crypto";
18 |
19 | Amplify.configure(config);
20 |
21 | const obj = { hello: "world" };
22 | const pairA = generateKeyPair();
23 | const pairB = generateKeyPair();
24 |
25 | const sharedA = box.before(pairB.publicKey, pairA.secretKey);
26 | const encrypted = encrypt(sharedA, obj);
27 |
28 | const sharedB = box.before(pairA.publicKey, pairB.secretKey);
29 | const decrypted = decrypt(sharedB, encrypted);
30 | console.log(obj, encrypted, decrypted);
31 |
32 | function App() {
33 | const isLoadingComplete = useCachedResources();
34 | const colorScheme = useColorScheme();
35 |
36 | const [user, setUser] = useState(null);
37 |
38 | useEffect(() => {
39 | const listener = Hub.listen("datastore", async (hubData) => {
40 | const { event, data } = hubData.payload;
41 | if (
42 | event === "outboxMutationProcessed" &&
43 | data.model === Message &&
44 | !["DELIVERED", "READ"].includes(data.element.status)
45 | ) {
46 | // set the message status to delivered
47 | DataStore.save(
48 | Message.copyOf(data.element, (updated) => {
49 | updated.status = "DELIVERED";
50 | })
51 | );
52 | }
53 | });
54 |
55 | // Remove listener
56 | return () => listener();
57 | }, []);
58 |
59 | useEffect(() => {
60 | if (!user) {
61 | return;
62 | }
63 |
64 | const subscription = DataStore.observe(User, user.id).subscribe((msg) => {
65 | if (msg.model === User && msg.opType === "UPDATE") {
66 | setUser(msg.element);
67 | }
68 | });
69 |
70 | return () => subscription.unsubscribe();
71 | }, [user?.id]);
72 |
73 | useEffect(() => {
74 | fetchUser();
75 | }, []);
76 |
77 | useEffect(() => {
78 | const interval = setInterval(async () => {
79 | await updateLastOnline();
80 | }, 1 * 60 * 1000);
81 | return () => clearInterval(interval);
82 | }, [user]);
83 |
84 | const fetchUser = async () => {
85 | const userData = await Auth.currentAuthenticatedUser();
86 | const user = await DataStore.query(User, userData.attributes.sub);
87 | if (user) {
88 | setUser(user);
89 | }
90 | };
91 |
92 | const updateLastOnline = async () => {
93 | if (!user) {
94 | return;
95 | }
96 |
97 | const response = await DataStore.save(
98 | User.copyOf(user, (updated) => {
99 | updated.lastOnlineAt = +new Date();
100 | })
101 | );
102 | setUser(response);
103 | };
104 |
105 | if (!isLoadingComplete) {
106 | return null;
107 | } else {
108 | return (
109 |
110 |
111 |
112 |
113 |
114 |
115 | );
116 | }
117 | }
118 |
119 | export default withAuthenticator(App);
120 |
--------------------------------------------------------------------------------
/components/MessageReply/MessageReply.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import {
3 | View,
4 | Text,
5 | StyleSheet,
6 | ActivityIndicator,
7 | Pressable,
8 | } from "react-native";
9 | import { DataStore } from "@aws-amplify/datastore";
10 | import { User } from "../../src/models";
11 | import { Auth, Storage } from "aws-amplify";
12 | import { S3Image } from "aws-amplify-react-native";
13 | import { useWindowDimensions } from "react-native";
14 | import { Ionicons } from "@expo/vector-icons";
15 | import AudioPlayer from "../AudioPlayer";
16 | import { Message as MessageModel } from "../../src/models";
17 |
18 | const blue = "#3777f0";
19 | const grey = "lightgrey";
20 |
21 | const MessageReply = (props) => {
22 | const { message: propMessage } = props;
23 |
24 | const [message, setMessage] = useState(propMessage);
25 |
26 | const [user, setUser] = useState();
27 | const [isMe, setIsMe] = useState(null);
28 | const [soundURI, setSoundURI] = useState(null);
29 |
30 | const { width } = useWindowDimensions();
31 |
32 | useEffect(() => {
33 | DataStore.query(User, message.userID).then(setUser);
34 | }, []);
35 |
36 | useEffect(() => {
37 | setMessage(propMessage);
38 | }, [propMessage]);
39 |
40 | useEffect(() => {
41 | if (message.audio) {
42 | Storage.get(message.audio).then(setSoundURI);
43 | }
44 | }, [message]);
45 |
46 | useEffect(() => {
47 | const checkIfMe = async () => {
48 | if (!user) {
49 | return;
50 | }
51 | const authUser = await Auth.currentAuthenticatedUser();
52 | setIsMe(user.id === authUser.attributes.sub);
53 | };
54 | checkIfMe();
55 | }, [user]);
56 |
57 | if (!user) {
58 | return ;
59 | }
60 |
61 | return (
62 |
69 |
70 | {message.image && (
71 |
72 |
77 |
78 | )}
79 | {soundURI && }
80 | {!!message.content && (
81 |
82 | {message.content}
83 |
84 | )}
85 |
86 | {isMe && !!message.status && message.status !== "SENT" && (
87 |
95 | )}
96 |
97 |
98 | );
99 | };
100 |
101 | const styles = StyleSheet.create({
102 | container: {
103 | padding: 10,
104 | margin: 10,
105 | borderRadius: 10,
106 | maxWidth: "75%",
107 | },
108 | row: {
109 | flexDirection: "row",
110 | alignItems: "flex-end",
111 | },
112 | messageReply: {
113 | backgroundColor: "gray",
114 | padding: 5,
115 | borderRadius: 5,
116 | },
117 | leftContainer: {
118 | backgroundColor: blue,
119 | marginLeft: 10,
120 | marginRight: "auto",
121 | },
122 | rightContainer: {
123 | backgroundColor: grey,
124 | marginLeft: "auto",
125 | marginRight: 10,
126 | alignItems: "flex-end",
127 | },
128 | });
129 |
130 | export default MessageReply;
131 |
--------------------------------------------------------------------------------
/navigation/index.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * If you are not familiar with React Navigation, check out the "Fundamentals" guide:
3 | * https://reactnavigation.org/docs/getting-started
4 | *
5 | */
6 | import {
7 | NavigationContainer,
8 | DefaultTheme,
9 | DarkTheme,
10 | useNavigation,
11 | } from "@react-navigation/native";
12 | import { createStackNavigator } from "@react-navigation/stack";
13 | import * as React from "react";
14 | import {
15 | ColorSchemeName,
16 | View,
17 | Text,
18 | Image,
19 | useWindowDimensions,
20 | Pressable,
21 | } from "react-native";
22 | import { Feather } from "@expo/vector-icons";
23 |
24 | import NotFoundScreen from "../screens/NotFoundScreen";
25 | import { RootStackParamList } from "../types";
26 | import LinkingConfiguration from "./LinkingConfiguration";
27 |
28 | import ChatRoomScreen from "../screens/ChatRoomScreen";
29 | import HomeScreen from "../screens/HomeScreen";
30 | import UsersScreen from "../screens/UsersScreen";
31 | import SettingsScreen from "../screens/Settings";
32 |
33 | import ChatRoomHeader from "./ChatRoomHeader";
34 | import GroupInfoScreen from "../screens/GroupInfoScreen";
35 |
36 | export default function Navigation({
37 | colorScheme,
38 | }: {
39 | colorScheme: ColorSchemeName;
40 | }) {
41 | return (
42 |
46 |
47 |
48 | );
49 | }
50 |
51 | // A root stack navigator is often used for displaying modals on top of all other content
52 | // Read more here: https://reactnavigation.org/docs/modal
53 | const Stack = createStackNavigator();
54 |
55 | function RootNavigator() {
56 | return (
57 |
58 |
63 | ({
67 | headerTitle: () => ,
68 | headerBackTitleVisible: false,
69 | })}
70 | />
71 |
72 |
79 |
80 |
81 |
86 |
87 | );
88 | }
89 |
90 | const HomeHeader = (props) => {
91 | const { width } = useWindowDimensions();
92 | const navigation = useNavigation();
93 |
94 | return (
95 |
104 |
110 |
118 | Signal
119 |
120 | navigation.navigate("Settings")}>
121 |
127 |
128 | navigation.navigate("UsersScreen")}>
129 |
135 |
136 |
137 | );
138 | };
139 |
--------------------------------------------------------------------------------
/assets/dummy-data/ChatRooms.ts:
--------------------------------------------------------------------------------
1 | export default [{
2 | id: '1',
3 | users: [{
4 | id: 'u1',
5 | name: 'Vadim',
6 | imageUri: 'https://notjustdev-dummy.s3.us-east-2.amazonaws.com/avatars/vadim.jpg',
7 | }, {
8 | id: 'u2',
9 | name: 'Elon Musk',
10 | imageUri: 'https://notjustdev-dummy.s3.us-east-2.amazonaws.com/avatars/elon.png',
11 | }],
12 | lastMessage: {
13 | id: 'm1',
14 | content: 'btw, SpaceX is interested in buying notJust.dev!',
15 | createdAt: '2020-10-03T14:48:00.000Z',
16 | },
17 | newMessages: 4,
18 | }, {
19 | id: '231231231231231',
20 | users: [{
21 | id: 'u1',
22 | name: 'Vadim',
23 | imageUri: 'https://notjustdev-dummy.s3.us-east-2.amazonaws.com/avatars/vadim.jpg',
24 | }, {
25 | id: 'u3',
26 | name: 'Jeff',
27 | imageUri: 'https://notjustdev-dummy.s3.us-east-2.amazonaws.com/avatars/jeff.jpeg',
28 | }],
29 | lastMessage: {
30 | id: 'm2',
31 | content: 'Why did you reject our offer?',
32 | createdAt: '2020-10-02T15:40:00.000Z',
33 | }
34 | }, {
35 | id: '3',
36 | users: [{
37 | id: 'u1',
38 | name: 'Vadim',
39 | imageUri: 'https://notjustdev-dummy.s3.us-east-2.amazonaws.com/avatars/vadim.jpg',
40 | }, {
41 | id: 'u4',
42 | name: 'Zuck',
43 | imageUri: 'https://notjustdev-dummy.s3.us-east-2.amazonaws.com/avatars/zuck.jpeg',
44 | }],
45 | lastMessage: {
46 | id: 'm3',
47 | content: 'Is signal really better than my Whatsapp?',
48 | createdAt: '2020-10-02T14:48:00.000Z',
49 | }
50 | }, {
51 | id: '4',
52 | users: [{
53 | id: 'u1',
54 | name: 'Vadim',
55 | imageUri: 'https://notjustdev-dummy.s3.us-east-2.amazonaws.com/avatars/vadim.jpg',
56 | }, {
57 | id: 'u5',
58 | name: 'Graham',
59 | imageUri: 'https://notjustdev-dummy.s3.us-east-2.amazonaws.com/avatars/graham.jpg',
60 | }],
61 | lastMessage: {
62 | id: 'm4',
63 | content: 'Destroy the like button!',
64 | createdAt: '2020-09-29T14:48:00.000Z',
65 | }
66 | }, {
67 | id: '5',
68 | users: [{
69 | id: 'u1',
70 | name: 'Vadim',
71 | imageUri: 'https://notjustdev-dummy.s3.us-east-2.amazonaws.com/avatars/vadim.jpg',
72 | }, {
73 | id: 'u6',
74 | name: 'Biahaze',
75 | imageUri: 'https://notjustdev-dummy.s3.us-east-2.amazonaws.com/avatars/biahaze.jpg',
76 | }],
77 | lastMessage: {
78 | id: 'm5',
79 | content: 'I would be happy',
80 | createdAt: '2020-09-30T14:48:00.000Z',
81 | }
82 | }, {
83 | id: '6',
84 | users: [{
85 | id: 'u1',
86 | name: 'Vadim',
87 | imageUri: 'https://notjustdev-dummy.s3.us-east-2.amazonaws.com/avatars/vadim.jpg',
88 | }, {
89 | id: 'u7',
90 | name: 'Sus?',
91 | imageUri: 'https://notjustdev-dummy.s3.us-east-2.amazonaws.com/avatars/1.jpg',
92 | }],
93 | lastMessage: {
94 | id: 'm6',
95 | content: 'Who sus?',
96 | createdAt: '2020-10-02T15:40:00.000Z',
97 | }
98 | }, {
99 | id: '7',
100 | users: [{
101 | id: 'u1',
102 | name: 'Vadim',
103 | imageUri: 'https://notjustdev-dummy.s3.us-east-2.amazonaws.com/avatars/vadim.jpg',
104 | }, {
105 | id: 'u8',
106 | name: 'Daniel',
107 | imageUri: 'https://notjustdev-dummy.s3.us-east-2.amazonaws.com/avatars/2.jpg',
108 | }],
109 | lastMessage: {
110 | id: 'm7',
111 | content: 'How are you doing?',
112 | createdAt: '2020-10-02T15:40:00.000Z',
113 | }
114 | }, {
115 | id: '8',
116 | users: [{
117 | id: 'u1',
118 | name: 'Vadim',
119 | imageUri: 'https://notjustdev-dummy.s3.us-east-2.amazonaws.com/avatars/vadim.jpg',
120 | }, {
121 | id: 'u9',
122 | name: 'Carlos',
123 | imageUri: 'https://notjustdev-dummy.s3.us-east-2.amazonaws.com/avatars/3.jpg',
124 | }],
125 | lastMessage: {
126 | id: 'm8',
127 | content: 'Hola hola coca cola?',
128 | createdAt: '2020-09-27T15:40:00.000Z',
129 | }
130 | }, {
131 | id: '9',
132 | users: [{
133 | id: 'u1',
134 | name: 'Vadim',
135 | imageUri: 'https://notjustdev-dummy.s3.us-east-2.amazonaws.com/avatars/vadim.jpg',
136 | }, {
137 | id: 'u10',
138 | name: 'Angelina Jolie',
139 | imageUri: 'https://notjustdev-dummy.s3.us-east-2.amazonaws.com/avatars/4.jpg',
140 | }],
141 | lastMessage: {
142 | id: 'm9',
143 | content: 'Meet me at the same place',
144 | createdAt: '2020-09-25T15:40:00.000Z',
145 | },
146 | }]
147 |
--------------------------------------------------------------------------------
/screens/UsersScreen.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 |
3 | import {
4 | View,
5 | StyleSheet,
6 | FlatList,
7 | Text,
8 | Pressable,
9 | SafeAreaView,
10 | Alert,
11 | } from "react-native";
12 | import UserItem from "../components/UserItem";
13 | import NewGroupButton from "../components/NewGroupButton";
14 | import { useNavigation } from "@react-navigation/native";
15 | import { Auth, DataStore } from "aws-amplify";
16 | import { ChatRoom, User, ChatRoomUser } from "../src/models";
17 |
18 | export default function UsersScreen() {
19 | const [users, setUsers] = useState([]);
20 | const [selectedUsers, setSelectedUsers] = useState([]);
21 | const [isNewGroup, setIsNewGroup] = useState(false);
22 |
23 | const navigation = useNavigation();
24 |
25 | useEffect(() => {
26 | DataStore.query(User).then(setUsers);
27 | }, []);
28 |
29 | // useEffect(() => {
30 | // // query users
31 | // const fetchUsers = async () => {
32 | // const fetchedUsers = await DataStore.query(User);
33 | // setUsers(fetchedUsers);
34 | // };
35 | // fetchUsers();
36 | // }, [])
37 |
38 | const addUserToChatRoom = async (user, chatroom) => {
39 | DataStore.save(new ChatRoomUser({ user, chatroom }));
40 | };
41 |
42 | const createChatRoom = async (users) => {
43 | // TODO if there is already a chat room between these 2 users
44 | // then redirect to the existing chat room
45 | // otherwise, create a new chatroom with these users.
46 |
47 | // connect authenticated user with the chat room
48 | const authUser = await Auth.currentAuthenticatedUser();
49 | const dbUser = await DataStore.query(User, authUser.attributes.sub);
50 | if (!dbUser) {
51 | Alert.alert("There was an error creating the group");
52 | return;
53 | }
54 | // Create a chat room
55 | const newChatRoomData = {
56 | newMessages: 0,
57 | Admin: dbUser,
58 | };
59 | if (users.length > 1) {
60 | newChatRoomData.name = "New group 2";
61 | newChatRoomData.imageUri =
62 | "https://notjustdev-dummy.s3.us-east-2.amazonaws.com/avatars/group.jpeg";
63 | }
64 | const newChatRoom = await DataStore.save(new ChatRoom(newChatRoomData));
65 |
66 | if (dbUser) {
67 | await addUserToChatRoom(dbUser, newChatRoom);
68 | }
69 |
70 | // connect users user with the chat room
71 | await Promise.all(
72 | users.map((user) => addUserToChatRoom(user, newChatRoom))
73 | );
74 |
75 | navigation.navigate("ChatRoom", { id: newChatRoom.id });
76 | };
77 |
78 | const isUserSelected = (user) => {
79 | return selectedUsers.some((selectedUser) => selectedUser.id === user.id);
80 | };
81 |
82 | const onUserPress = async (user) => {
83 | if (isNewGroup) {
84 | if (isUserSelected(user)) {
85 | // remove it from selected
86 | setSelectedUsers(
87 | selectedUsers.filter((selectedUser) => selectedUser.id !== user.id)
88 | );
89 | } else {
90 | setSelectedUsers([...selectedUsers, user]);
91 | }
92 | } else {
93 | await createChatRoom([user]);
94 | }
95 | };
96 |
97 | const saveGroup = async () => {
98 | await createChatRoom(selectedUsers);
99 | };
100 |
101 | return (
102 |
103 | (
106 | onUserPress(item)}
109 | isSelected={isNewGroup ? isUserSelected(item) : undefined}
110 | />
111 | )}
112 | showsVerticalScrollIndicator={false}
113 | ListHeaderComponent={() => (
114 | setIsNewGroup(!isNewGroup)} />
115 | )}
116 | />
117 |
118 | {isNewGroup && (
119 |
120 |
121 | Save group ({selectedUsers.length})
122 |
123 |
124 | )}
125 |
126 | );
127 | }
128 |
129 | const styles = StyleSheet.create({
130 | page: {
131 | backgroundColor: "white",
132 | flex: 1,
133 | },
134 | button: {
135 | backgroundColor: "#3777f0",
136 | marginHorizontal: 10,
137 | padding: 10,
138 | alignItems: "center",
139 | borderRadius: 10,
140 | },
141 | buttonText: {
142 | color: "white",
143 | fontWeight: "bold",
144 | },
145 | });
146 |
--------------------------------------------------------------------------------
/amplify/backend/auth/SignalClone/auth-trigger-cloudformation-template.json:
--------------------------------------------------------------------------------
1 | {
2 | "Description": "Custom Resource stack for Auth Trigger created using Amplify CLI",
3 | "AWSTemplateFormatVersion": "2010-09-09",
4 | "Parameters": {
5 | "env": {
6 | "Type": "String"
7 | },
8 | "userpoolId": {
9 | "Type": "String"
10 | },
11 | "userpoolArn": {
12 | "Type": "String"
13 | },
14 | "functionSignalClonePostConfirmationName": {
15 | "Type": "String"
16 | },
17 | "functionSignalClonePostConfirmationArn": {
18 | "Type": "String"
19 | }
20 | },
21 | "Conditions": {
22 | "ShouldNotCreateEnvResources": {
23 | "Fn::Equals": [
24 | {
25 | "Ref": "env"
26 | },
27 | "NONE"
28 | ]
29 | }
30 | },
31 | "Resources": {
32 | "UserPoolPostConfirmationLambdaInvokePermission": {
33 | "Type": "AWS::Lambda::Permission",
34 | "Properties": {
35 | "Action": "lambda:InvokeFunction",
36 | "FunctionName": {
37 | "Ref": "functionSignalClonePostConfirmationName"
38 | },
39 | "Principal": "cognito-idp.amazonaws.com",
40 | "SourceArn": {
41 | "Ref": "userpoolArn"
42 | }
43 | }
44 | },
45 | "authTriggerFnServiceRole08093B67": {
46 | "Type": "AWS::IAM::Role",
47 | "Properties": {
48 | "AssumeRolePolicyDocument": {
49 | "Statement": [
50 | {
51 | "Action": "sts:AssumeRole",
52 | "Effect": "Allow",
53 | "Principal": {
54 | "Service": "lambda.amazonaws.com"
55 | }
56 | }
57 | ],
58 | "Version": "2012-10-17"
59 | },
60 | "ManagedPolicyArns": [
61 | {
62 | "Fn::Join": [
63 | "",
64 | [
65 | "arn:",
66 | {
67 | "Ref": "AWS::Partition"
68 | },
69 | ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
70 | ]
71 | ]
72 | }
73 | ]
74 | }
75 | },
76 | "authTriggerFnServiceRoleDefaultPolicyEC9285A8": {
77 | "Type": "AWS::IAM::Policy",
78 | "Properties": {
79 | "PolicyDocument": {
80 | "Statement": [
81 | {
82 | "Action": [
83 | "cognito-idp:DescribeUserPoolClient",
84 | "cognito-idp:UpdateUserPool"
85 | ],
86 | "Effect": "Allow",
87 | "Resource": "*"
88 | }
89 | ],
90 | "Version": "2012-10-17"
91 | },
92 | "PolicyName": "authTriggerFnServiceRoleDefaultPolicyEC9285A8",
93 | "Roles": [
94 | {
95 | "Ref": "authTriggerFnServiceRole08093B67"
96 | }
97 | ]
98 | }
99 | },
100 | "authTriggerFn7FCFA449": {
101 | "Type": "AWS::Lambda::Function",
102 | "Properties": {
103 | "Code": {
104 | "ZipFile": "const response = require('cfn-response');\nconst aws = require('aws-sdk');\n\nexports.handler = async function (event, context) {\n try {\n const userPoolId = event.ResourceProperties.userpoolId;\n const lambdaConfig = event.ResourceProperties.lambdaConfig;\n const config = {};\n lambdaConfig.forEach(lambda => (config[`${lambda.triggerType}`] = lambda.lambdaFunctionArn));\n if (event.RequestType == 'Delete') {\n const authParams = { UserPoolId: userPoolId, LambdaConfig: {} };\n const cognitoclient = new aws.CognitoIdentityServiceProvider();\n try {\n const result = await cognitoclient.updateUserPool(authParams).promise();\n console.log('delete response data ' + JSON.stringify(result));\n await response.send(event, context, response.SUCCESS, {});\n } catch (err) {\n console.log(err.stack);\n await response.send(event, context, response.FAILED, { err });\n }\n }\n if (event.RequestType == 'Update' || event.RequestType == 'Create') {\n const authParams = { UserPoolId: userPoolId, LambdaConfig: config };\n console.log(authParams);\n const cognitoclient = new aws.CognitoIdentityServiceProvider();\n try {\n const result = await cognitoclient.updateUserPool(authParams).promise();\n console.log('createOrUpdate response data ' + JSON.stringify(result));\n await response.send(event, context, response.SUCCESS, { result });\n } catch (err) {\n console.log(err.stack);\n await response.send(event, context, response.FAILED, { err });\n }\n }\n } catch (err) {\n console.log(err.stack);\n await response.send(event, context, response.FAILED, { err });\n }\n};\n"
105 | },
106 | "Handler": "index.handler",
107 | "Role": {
108 | "Fn::GetAtt": [
109 | "authTriggerFnServiceRole08093B67",
110 | "Arn"
111 | ]
112 | },
113 | "Runtime": "nodejs12.x"
114 | },
115 | "DependsOn": [
116 | "authTriggerFnServiceRoleDefaultPolicyEC9285A8",
117 | "authTriggerFnServiceRole08093B67"
118 | ]
119 | },
120 | "CustomAuthTriggerResource": {
121 | "Type": "Custom::CustomAuthTriggerResourceOutputs",
122 | "Properties": {
123 | "ServiceToken": {
124 | "Fn::GetAtt": [
125 | "authTriggerFn7FCFA449",
126 | "Arn"
127 | ]
128 | },
129 | "userpoolId": {
130 | "Ref": "userpoolId"
131 | },
132 | "lambdaConfig": [
133 | {
134 | "triggerType": "PostConfirmation",
135 | "lambdaFunctionName": "SignalClonePostConfirmation",
136 | "lambdaFunctionArn": {
137 | "Ref": "functionSignalClonePostConfirmationArn"
138 | }
139 | }
140 | ]
141 | },
142 | "UpdateReplacePolicy": "Delete",
143 | "DeletionPolicy": "Delete"
144 | }
145 | }
146 | }
--------------------------------------------------------------------------------
/amplify/backend/function/SignalClonePostConfirmation/SignalClonePostConfirmation-cloudformation-template.json:
--------------------------------------------------------------------------------
1 | {
2 | "AWSTemplateFormatVersion": "2010-09-09",
3 | "Description": "Lambda resource stack creation using Amplify CLI",
4 | "Parameters": {
5 | "GROUP": {
6 | "Type": "String",
7 | "Default": ""
8 | },
9 | "modules": {
10 | "Type": "String",
11 | "Default": "",
12 | "Description": "Comma-delimited list of modules to be executed by a lambda trigger. Sent to resource as an env variable."
13 | },
14 | "resourceName": {
15 | "Type": "String",
16 | "Default": ""
17 | },
18 | "trigger": {
19 | "Type": "String",
20 | "Default": "true"
21 | },
22 | "functionName": {
23 | "Type": "String",
24 | "Default": ""
25 | },
26 | "roleName": {
27 | "Type": "String",
28 | "Default": ""
29 | },
30 | "parentResource": {
31 | "Type": "String",
32 | "Default": ""
33 | },
34 | "parentStack": {
35 | "Type": "String",
36 | "Default": ""
37 | },
38 | "env": {
39 | "Type": "String"
40 | },
41 | "deploymentBucketName": {
42 | "Type": "String"
43 | },
44 | "s3Key": {
45 | "Type": "String"
46 | }
47 | },
48 | "Conditions": {
49 | "ShouldNotCreateEnvResources": {
50 | "Fn::Equals": [
51 | {
52 | "Ref": "env"
53 | },
54 | "NONE"
55 | ]
56 | }
57 | },
58 | "Resources": {
59 | "LambdaFunction": {
60 | "Type": "AWS::Lambda::Function",
61 | "Metadata": {
62 | "aws:asset:path": "./src",
63 | "aws:asset:property": "Code"
64 | },
65 | "Properties": {
66 | "Handler": "index.handler",
67 | "FunctionName": {
68 | "Fn::If": [
69 | "ShouldNotCreateEnvResources",
70 | "SignalClonePostConfirmation",
71 | {
72 | "Fn::Join": [
73 | "",
74 | [
75 | "SignalClonePostConfirmation",
76 | "-",
77 | {
78 | "Ref": "env"
79 | }
80 | ]
81 | ]
82 | }
83 | ]
84 | },
85 | "Environment": {
86 | "Variables": {
87 | "ENV": {
88 | "Ref": "env"
89 | },
90 | "MODULES": {
91 | "Ref": "modules"
92 | },
93 | "REGION": {
94 | "Ref": "AWS::Region"
95 | },
96 | "GROUP": {
97 | "Ref": "GROUP"
98 | }
99 | }
100 | },
101 | "Role": {
102 | "Fn::GetAtt": [
103 | "LambdaExecutionRole",
104 | "Arn"
105 | ]
106 | },
107 | "Runtime": "nodejs14.x",
108 | "Timeout": 25,
109 | "Code": {
110 | "S3Bucket": {
111 | "Ref": "deploymentBucketName"
112 | },
113 | "S3Key": {
114 | "Ref": "s3Key"
115 | }
116 | }
117 | }
118 | },
119 | "LambdaExecutionRole": {
120 | "Type": "AWS::IAM::Role",
121 | "Properties": {
122 | "RoleName": {
123 | "Fn::If": [
124 | "ShouldNotCreateEnvResources",
125 | "SignalClonePostConfirmation",
126 | {
127 | "Fn::Join": [
128 | "",
129 | [
130 | "SignalClonePostConfirmation",
131 | "-",
132 | {
133 | "Ref": "env"
134 | }
135 | ]
136 | ]
137 | }
138 | ]
139 | },
140 | "AssumeRolePolicyDocument": {
141 | "Version": "2012-10-17",
142 | "Statement": [
143 | {
144 | "Effect": "Allow",
145 | "Principal": {
146 | "Service": [
147 | "lambda.amazonaws.com"
148 | ]
149 | },
150 | "Action": [
151 | "sts:AssumeRole"
152 | ]
153 | }
154 | ]
155 | }
156 | }
157 | },
158 | "lambdaexecutionpolicy": {
159 | "DependsOn": [
160 | "LambdaExecutionRole"
161 | ],
162 | "Type": "AWS::IAM::Policy",
163 | "Properties": {
164 | "PolicyName": "lambda-execution-policy",
165 | "Roles": [
166 | {
167 | "Ref": "LambdaExecutionRole"
168 | }
169 | ],
170 | "PolicyDocument": {
171 | "Version": "2012-10-17",
172 | "Statement": [
173 | {
174 | "Effect": "Allow",
175 | "Action": [
176 | "logs:CreateLogGroup",
177 | "logs:CreateLogStream",
178 | "logs:PutLogEvents"
179 | ],
180 | "Resource": {
181 | "Fn::Sub": [
182 | "arn:aws:logs:${region}:${account}:log-group:/aws/lambda/${lambda}:log-stream:*",
183 | {
184 | "region": {
185 | "Ref": "AWS::Region"
186 | },
187 | "account": {
188 | "Ref": "AWS::AccountId"
189 | },
190 | "lambda": {
191 | "Ref": "LambdaFunction"
192 | }
193 | }
194 | ]
195 | }
196 | }
197 | ]
198 | }
199 | }
200 | }
201 | },
202 | "Outputs": {
203 | "Name": {
204 | "Value": {
205 | "Ref": "LambdaFunction"
206 | }
207 | },
208 | "Arn": {
209 | "Value": {
210 | "Fn::GetAtt": [
211 | "LambdaFunction",
212 | "Arn"
213 | ]
214 | }
215 | },
216 | "LambdaExecutionRole": {
217 | "Value": {
218 | "Ref": "LambdaExecutionRole"
219 | }
220 | },
221 | "Region": {
222 | "Value": {
223 | "Ref": "AWS::Region"
224 | }
225 | }
226 | }
227 | }
--------------------------------------------------------------------------------
/components/Message/Message.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import {
3 | View,
4 | Text,
5 | StyleSheet,
6 | ActivityIndicator,
7 | Pressable,
8 | Alert,
9 | } from "react-native";
10 | import { DataStore } from "@aws-amplify/datastore";
11 | import { User } from "../../src/models";
12 | import { Auth, Storage } from "aws-amplify";
13 | import { S3Image } from "aws-amplify-react-native";
14 | import { useWindowDimensions } from "react-native";
15 | import { Ionicons } from "@expo/vector-icons";
16 | import { useActionSheet } from "@expo/react-native-action-sheet";
17 | import AudioPlayer from "../AudioPlayer";
18 | import { Message as MessageModel } from "../../src/models";
19 | import MessageReply from "../MessageReply";
20 | import { box } from "tweetnacl";
21 | import {
22 | decrypt,
23 | getMySecretKey,
24 | stringToUint8Array,
25 | } from "../../utils/crypto";
26 |
27 | const blue = "#3777f0";
28 | const grey = "lightgrey";
29 |
30 | const Message = (props) => {
31 | const { setAsMessageReply, message: propMessage } = props;
32 |
33 | const [message, setMessage] = useState(propMessage);
34 | const [decryptedContent, setDecryptedContent] = useState("");
35 | const [repliedTo, setRepliedTo] = useState(
36 | undefined
37 | );
38 | const [user, setUser] = useState();
39 | const [isMe, setIsMe] = useState(null);
40 | const [soundURI, setSoundURI] = useState(null);
41 | const [isDeleted, setIsDeleted] = useState(false);
42 |
43 | const { width } = useWindowDimensions();
44 | const { showActionSheetWithOptions } = useActionSheet();
45 |
46 | useEffect(() => {
47 | DataStore.query(User, message.userID).then(setUser);
48 | }, []);
49 |
50 | useEffect(() => {
51 | setMessage(propMessage);
52 | }, [propMessage]);
53 |
54 | useEffect(() => {
55 | if (message?.replyToMessageID) {
56 | DataStore.query(MessageModel, message.replyToMessageID).then(
57 | setRepliedTo
58 | );
59 | }
60 | }, [message]);
61 |
62 | useEffect(() => {
63 | const subscription = DataStore.observe(MessageModel, message.id).subscribe(
64 | (msg) => {
65 | if (msg.model === MessageModel) {
66 | if (msg.opType === "UPDATE") {
67 | setMessage((message) => ({ ...message, ...msg.element }));
68 | } else if (msg.opType === "DELETE") {
69 | setIsDeleted(true);
70 | }
71 | }
72 | }
73 | );
74 |
75 | return () => subscription.unsubscribe();
76 | }, []);
77 |
78 | useEffect(() => {
79 | setAsRead();
80 | }, [isMe, message]);
81 |
82 | useEffect(() => {
83 | if (message.audio) {
84 | Storage.get(message.audio).then(setSoundURI);
85 | }
86 | }, [message]);
87 |
88 | useEffect(() => {
89 | const checkIfMe = async () => {
90 | if (!user) {
91 | return;
92 | }
93 | const authUser = await Auth.currentAuthenticatedUser();
94 | setIsMe(user.id === authUser.attributes.sub);
95 | };
96 | checkIfMe();
97 | }, [user]);
98 |
99 | useEffect(() => {
100 | if (!message?.content || !user?.publicKey) {
101 | return;
102 | }
103 |
104 | const decryptMessage = async () => {
105 | const myKey = await getMySecretKey();
106 | if (!myKey) {
107 | return;
108 | }
109 | // decrypt message.content
110 | const sharedKey = box.before(stringToUint8Array(user.publicKey), myKey);
111 | console.log("sharedKey", sharedKey);
112 | const decrypted = decrypt(sharedKey, message.content);
113 | console.log("decrypted", decrypted);
114 | setDecryptedContent(decrypted.message);
115 | };
116 |
117 | decryptMessage();
118 | }, [message, user]);
119 |
120 | const setAsRead = async () => {
121 | if (isMe === false && message.status !== "READ") {
122 | await DataStore.save(
123 | MessageModel.copyOf(message, (updated) => {
124 | updated.status = "READ";
125 | })
126 | );
127 | }
128 | };
129 |
130 | const deleteMessage = async () => {
131 | await DataStore.delete(message);
132 | };
133 |
134 | const confirmDelete = () => {
135 | Alert.alert(
136 | "Confirm delete",
137 | "Are you sure you want to delete the message?",
138 | [
139 | {
140 | text: "Delete",
141 | onPress: deleteMessage,
142 | style: "destructive",
143 | },
144 | {
145 | text: "Cancel",
146 | },
147 | ]
148 | );
149 | };
150 |
151 | const onActionPress = (index) => {
152 | if (index === 0) {
153 | setAsMessageReply();
154 | } else if (index === 1) {
155 | if (isMe) {
156 | confirmDelete();
157 | } else {
158 | Alert.alert("Can't perform action", "This is not your message");
159 | }
160 | }
161 | };
162 |
163 | const openActionMenu = () => {
164 | const options = ["Reply", "Delete", "Cancel"];
165 | const destructiveButtonIndex = 1;
166 | const cancelButtonIndex = 2;
167 | showActionSheetWithOptions(
168 | {
169 | options,
170 | destructiveButtonIndex,
171 | cancelButtonIndex,
172 | },
173 | onActionPress
174 | );
175 | };
176 |
177 | if (!user) {
178 | return ;
179 | }
180 |
181 | return (
182 |
190 | {repliedTo && }
191 |
192 | {message.image && (
193 |
194 |
199 |
200 | )}
201 | {soundURI && }
202 | {!!decryptedContent && (
203 |
204 | {isDeleted ? "message deleted" : decryptedContent}
205 |
206 | )}
207 |
208 | {isMe && !!message.status && message.status !== "SENT" && (
209 |
217 | )}
218 |
219 |
220 | );
221 | };
222 |
223 | const styles = StyleSheet.create({
224 | container: {
225 | padding: 10,
226 | margin: 10,
227 | borderRadius: 10,
228 | maxWidth: "75%",
229 | },
230 | row: {
231 | flexDirection: "row",
232 | alignItems: "flex-end",
233 | },
234 | messageReply: {
235 | backgroundColor: "gray",
236 | padding: 5,
237 | borderRadius: 5,
238 | },
239 | leftContainer: {
240 | backgroundColor: blue,
241 | marginLeft: 10,
242 | marginRight: "auto",
243 | },
244 | rightContainer: {
245 | backgroundColor: grey,
246 | marginLeft: "auto",
247 | marginRight: 10,
248 | alignItems: "flex-end",
249 | },
250 | });
251 |
252 | export default Message;
253 |
--------------------------------------------------------------------------------
/components/MessageInput/MessageInput.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import {
3 | View,
4 | Text,
5 | StyleSheet,
6 | TextInput,
7 | Pressable,
8 | KeyboardAvoidingView,
9 | Platform,
10 | Image,
11 | Alert,
12 | } from "react-native";
13 | import {
14 | SimpleLineIcons,
15 | Feather,
16 | MaterialCommunityIcons,
17 | AntDesign,
18 | Ionicons,
19 | } from "@expo/vector-icons";
20 | import { DataStore } from "@aws-amplify/datastore";
21 | import { ChatRoom, Message } from "../../src/models";
22 | import { Auth, Storage } from "aws-amplify";
23 | import EmojiSelector from "react-native-emoji-selector";
24 | import * as ImagePicker from "expo-image-picker";
25 | import { v4 as uuidv4 } from "uuid";
26 | import { Audio, AVPlaybackStatus } from "expo-av";
27 | import AudioPlayer from "../AudioPlayer";
28 | import MessageComponent from "../Message";
29 | import { ChatRoomUser } from "../../src/models";
30 | import { useNavigation } from "@react-navigation/core";
31 | import { box } from "tweetnacl";
32 | import {
33 | encrypt,
34 | getMySecretKey,
35 | stringToUint8Array,
36 | } from "../../utils/crypto";
37 |
38 | const MessageInput = ({ chatRoom, messageReplyTo, removeMessageReplyTo }) => {
39 | const [message, setMessage] = useState("");
40 | const [isEmojiPickerOpen, setIsEmojiPickerOpen] = useState(false);
41 | const [image, setImage] = useState(null);
42 | const [progress, setProgress] = useState(0);
43 | const [recording, setRecording] = useState(null);
44 | const [soundURI, setSoundURI] = useState(null);
45 |
46 | const navigation = useNavigation();
47 |
48 | useEffect(() => {
49 | (async () => {
50 | if (Platform.OS !== "web") {
51 | const libraryResponse =
52 | await ImagePicker.requestMediaLibraryPermissionsAsync();
53 | const photoResponse = await ImagePicker.requestCameraPermissionsAsync();
54 | await Audio.requestPermissionsAsync();
55 |
56 | if (
57 | libraryResponse.status !== "granted" ||
58 | photoResponse.status !== "granted"
59 | ) {
60 | alert("Sorry, we need camera roll permissions to make this work!");
61 | }
62 | }
63 | })();
64 | }, []);
65 |
66 | const sendMessageToUser = async (user, fromUserId) => {
67 | // send message
68 | const ourSecretKey = await getMySecretKey();
69 | if (!ourSecretKey) {
70 | return;
71 | }
72 |
73 | if (!user.publicKey) {
74 | Alert.alert(
75 | "The user haven't set his keypair yet",
76 | "Until the user generates the keypair, you cannot securely send him messages"
77 | );
78 | return;
79 | }
80 |
81 | console.log("private key", ourSecretKey);
82 |
83 | const sharedKey = box.before(
84 | stringToUint8Array(user.publicKey),
85 | ourSecretKey
86 | );
87 | console.log("shared key", sharedKey);
88 |
89 | const encryptedMessage = encrypt(sharedKey, { message });
90 | console.log("encrypted message", encryptedMessage);
91 |
92 | const newMessage = await DataStore.save(
93 | new Message({
94 | content: encryptedMessage, // <- this messages should be encrypted
95 | userID: fromUserId,
96 | forUserId: user.id,
97 | chatroomID: chatRoom.id,
98 | replyToMessageID: messageReplyTo?.id,
99 | })
100 | );
101 |
102 | // updateLastMessage(newMessage);
103 | };
104 |
105 | const sendMessage = async () => {
106 | // get all the users of this chatroom
107 | const authUser = await Auth.currentAuthenticatedUser();
108 |
109 | const users = (await DataStore.query(ChatRoomUser))
110 | .filter((cru) => cru.chatroom.id === chatRoom.id)
111 | .map((cru) => cru.user);
112 |
113 | console.log("users", users);
114 |
115 | // for each user, encrypt the `content` with his public key, and save it as a new message
116 | await Promise.all(
117 | users.map((user) => sendMessageToUser(user, authUser.attributes.sub))
118 | );
119 |
120 | resetFields();
121 | };
122 |
123 | const updateLastMessage = async (newMessage) => {
124 | DataStore.save(
125 | ChatRoom.copyOf(chatRoom, (updatedChatRoom) => {
126 | updatedChatRoom.LastMessage = newMessage;
127 | })
128 | );
129 | };
130 |
131 | const onPlusClicked = () => {
132 | console.warn("On plus clicked");
133 | };
134 |
135 | const onPress = () => {
136 | if (image) {
137 | sendImage();
138 | } else if (soundURI) {
139 | sendAudio();
140 | } else if (message) {
141 | sendMessage();
142 | } else {
143 | onPlusClicked();
144 | }
145 | };
146 |
147 | const resetFields = () => {
148 | setMessage("");
149 | setIsEmojiPickerOpen(false);
150 | setImage(null);
151 | setProgress(0);
152 | setSoundURI(null);
153 | removeMessageReplyTo();
154 | };
155 |
156 | // Image picker
157 | const pickImage = async () => {
158 | const result = await ImagePicker.launchImageLibraryAsync({
159 | mediaTypes: ImagePicker.MediaTypeOptions.Images,
160 | allowsEditing: true,
161 | aspect: [4, 3],
162 | quality: 0.5,
163 | });
164 |
165 | if (!result.cancelled) {
166 | setImage(result.uri);
167 | }
168 | };
169 |
170 | const takePhoto = async () => {
171 | const result = await ImagePicker.launchCameraAsync({
172 | mediaTypes: ImagePicker.MediaTypeOptions.Images,
173 | aspect: [4, 3],
174 | });
175 |
176 | if (!result.cancelled) {
177 | setImage(result.uri);
178 | }
179 | };
180 |
181 | const progressCallback = (progress) => {
182 | setProgress(progress.loaded / progress.total);
183 | };
184 |
185 | const sendImage = async () => {
186 | if (!image) {
187 | return;
188 | }
189 | const blob = await getBlob(image);
190 | const { key } = await Storage.put(`${uuidv4()}.png`, blob, {
191 | progressCallback,
192 | });
193 |
194 | // send message
195 | const user = await Auth.currentAuthenticatedUser();
196 | const newMessage = await DataStore.save(
197 | new Message({
198 | content: message,
199 | image: key,
200 | userID: user.attributes.sub,
201 | chatroomID: chatRoom.id,
202 | replyToMessageID: messageReplyTo?.id,
203 | })
204 | );
205 |
206 | updateLastMessage(newMessage);
207 |
208 | resetFields();
209 | };
210 |
211 | const getBlob = async (uri: string) => {
212 | const respone = await fetch(uri);
213 | const blob = await respone.blob();
214 | return blob;
215 | };
216 |
217 | async function startRecording() {
218 | try {
219 | await Audio.setAudioModeAsync({
220 | allowsRecordingIOS: true,
221 | playsInSilentModeIOS: true,
222 | });
223 |
224 | console.log("Starting recording..");
225 | const { recording } = await Audio.Recording.createAsync(
226 | Audio.RECORDING_OPTIONS_PRESET_HIGH_QUALITY
227 | );
228 | setRecording(recording);
229 | console.log("Recording started");
230 | } catch (err) {
231 | console.error("Failed to start recording", err);
232 | }
233 | }
234 |
235 | async function stopRecording() {
236 | console.log("Stopping recording..");
237 | if (!recording) {
238 | return;
239 | }
240 |
241 | setRecording(null);
242 | await recording.stopAndUnloadAsync();
243 | await Audio.setAudioModeAsync({
244 | allowsRecordingIOS: false,
245 | });
246 |
247 | const uri = recording.getURI();
248 | console.log("Recording stopped and stored at", uri);
249 | if (!uri) {
250 | return;
251 | }
252 | setSoundURI(uri);
253 | }
254 |
255 | const sendAudio = async () => {
256 | if (!soundURI) {
257 | return;
258 | }
259 | const uriParts = soundURI.split(".");
260 | const extenstion = uriParts[uriParts.length - 1];
261 | const blob = await getBlob(soundURI);
262 | const { key } = await Storage.put(`${uuidv4()}.${extenstion}`, blob, {
263 | progressCallback,
264 | });
265 |
266 | // send message
267 | const user = await Auth.currentAuthenticatedUser();
268 | const newMessage = await DataStore.save(
269 | new Message({
270 | content: message,
271 | audio: key,
272 | userID: user.attributes.sub,
273 | chatroomID: chatRoom.id,
274 | status: "SENT",
275 | replyToMessageID: messageReplyTo?.id,
276 | })
277 | );
278 |
279 | updateLastMessage(newMessage);
280 |
281 | resetFields();
282 | };
283 |
284 | return (
285 |
290 | {messageReplyTo && (
291 |
300 |
301 | Reply to:
302 |
303 |
304 | removeMessageReplyTo()}>
305 |
311 |
312 |
313 | )}
314 |
315 | {image && (
316 |
317 |
321 |
322 |
329 |
337 |
338 |
339 | setImage(null)}>
340 |
346 |
347 |
348 | )}
349 |
350 | {soundURI && }
351 |
352 |
353 |
354 |
356 | setIsEmojiPickerOpen((currentValue) => !currentValue)
357 | }
358 | >
359 |
365 |
366 |
367 |
373 |
374 |
375 |
381 |
382 |
383 |
384 |
390 |
391 |
392 |
393 |
399 |
400 |
401 |
402 |
403 | {message || image || soundURI ? (
404 |
405 | ) : (
406 |
407 | )}
408 |
409 |
410 |
411 | {isEmojiPickerOpen && (
412 |
414 | setMessage((currentMessage) => currentMessage + emoji)
415 | }
416 | columns={8}
417 | />
418 | )}
419 |
420 | );
421 | };
422 |
423 | const styles = StyleSheet.create({
424 | root: {
425 | padding: 10,
426 | },
427 | row: {
428 | flexDirection: "row",
429 | },
430 | inputContainer: {
431 | backgroundColor: "#f2f2f2",
432 | flex: 1,
433 | marginRight: 10,
434 | borderRadius: 25,
435 | borderWidth: 1,
436 | borderColor: "#dedede",
437 | alignItems: "center",
438 | flexDirection: "row",
439 | padding: 5,
440 | },
441 | input: {
442 | flex: 1,
443 | marginHorizontal: 5,
444 | },
445 | icon: {
446 | marginHorizontal: 5,
447 | },
448 | buttonContainer: {
449 | width: 40,
450 | height: 40,
451 | backgroundColor: "#3777f0",
452 | borderRadius: 25,
453 | justifyContent: "center",
454 | alignItems: "center",
455 | },
456 | buttonText: {
457 | color: "white",
458 | fontSize: 35,
459 | },
460 |
461 | sendImageContainer: {
462 | flexDirection: "row",
463 | marginVertical: 10,
464 | alignSelf: "stretch",
465 | justifyContent: "space-between",
466 | borderWidth: 1,
467 | borderColor: "lightgray",
468 | borderRadius: 10,
469 | },
470 | });
471 |
472 | export default MessageInput;
473 |
--------------------------------------------------------------------------------
/amplify/backend/auth/SignalClone/SignalClone-cloudformation-template.yml:
--------------------------------------------------------------------------------
1 |
2 | AWSTemplateFormatVersion: 2010-09-09
3 |
4 | Parameters:
5 | env:
6 | Type: String
7 | authRoleArn:
8 | Type: String
9 | unauthRoleArn:
10 | Type: String
11 |
12 |
13 |
14 |
15 | functionSignalClonePostConfirmationArn:
16 | Type: String
17 | Default: functionSignalClonePostConfirmationArn
18 |
19 | functionSignalClonePostConfirmationName:
20 | Type: String
21 | Default: functionSignalClonePostConfirmationName
22 |
23 |
24 |
25 |
26 |
27 | identityPoolName:
28 | Type: String
29 |
30 |
31 |
32 | allowUnauthenticatedIdentities:
33 | Type: String
34 |
35 | resourceNameTruncated:
36 | Type: String
37 |
38 |
39 | userPoolName:
40 | Type: String
41 |
42 |
43 |
44 | autoVerifiedAttributes:
45 | Type: CommaDelimitedList
46 |
47 | mfaConfiguration:
48 | Type: String
49 |
50 |
51 |
52 | mfaTypes:
53 | Type: CommaDelimitedList
54 |
55 | smsAuthenticationMessage:
56 | Type: String
57 |
58 |
59 | smsVerificationMessage:
60 | Type: String
61 |
62 |
63 | emailVerificationSubject:
64 | Type: String
65 |
66 |
67 | emailVerificationMessage:
68 | Type: String
69 |
70 |
71 |
72 | defaultPasswordPolicy:
73 | Type: String
74 |
75 |
76 | passwordPolicyMinLength:
77 | Type: Number
78 |
79 |
80 | passwordPolicyCharacters:
81 | Type: CommaDelimitedList
82 |
83 |
84 | requiredAttributes:
85 | Type: CommaDelimitedList
86 |
87 |
88 | aliasAttributes:
89 | Type: CommaDelimitedList
90 |
91 |
92 | userpoolClientGenerateSecret:
93 | Type: String
94 |
95 |
96 | userpoolClientRefreshTokenValidity:
97 | Type: Number
98 |
99 |
100 | userpoolClientWriteAttributes:
101 | Type: CommaDelimitedList
102 |
103 |
104 | userpoolClientReadAttributes:
105 | Type: CommaDelimitedList
106 |
107 | userpoolClientLambdaRole:
108 | Type: String
109 |
110 |
111 |
112 | userpoolClientSetAttributes:
113 | Type: String
114 |
115 | sharedId:
116 | Type: String
117 |
118 |
119 | resourceName:
120 | Type: String
121 |
122 |
123 | authSelections:
124 | Type: String
125 |
126 |
127 |
128 |
129 | serviceName:
130 | Type: String
131 |
132 |
133 |
134 | usernameAttributes:
135 | Type: CommaDelimitedList
136 |
137 | useDefault:
138 | Type: String
139 |
140 |
141 |
142 | userPoolGroups:
143 | Type: String
144 |
145 |
146 | userPoolGroupList:
147 | Type: CommaDelimitedList
148 |
149 |
150 | adminQueries:
151 | Type: String
152 |
153 |
154 | thirdPartyAuth:
155 | Type: String
156 |
157 |
158 | authProviders:
159 | Type: CommaDelimitedList
160 |
161 |
162 | usernameCaseSensitive:
163 | Type: String
164 |
165 |
166 | dependsOn:
167 | Type: CommaDelimitedList
168 |
169 | triggers:
170 | Type: String
171 |
172 |
173 |
174 | hostedUI:
175 | Type: String
176 |
177 |
178 |
179 | parentStack:
180 | Type: String
181 |
182 | authTriggerConnections:
183 | Type: String
184 |
185 |
186 |
187 | breakCircularDependency:
188 | Type: String
189 |
190 |
191 | permissions:
192 | Type: CommaDelimitedList
193 |
194 | Conditions:
195 | ShouldNotCreateEnvResources: !Equals [ !Ref env, NONE ]
196 |
197 | ShouldOutputAppClientSecrets: !Equals [!Ref userpoolClientGenerateSecret, true ]
198 |
199 |
200 | Resources:
201 |
202 |
203 | # BEGIN SNS ROLE RESOURCE
204 | SNSRole:
205 | # Created to allow the UserPool SMS Config to publish via the Simple Notification Service during MFA Process
206 | Type: AWS::IAM::Role
207 | Properties:
208 | RoleName: !If [ShouldNotCreateEnvResources, 'signal29db2920_sns-role', !Join ['',[ 'sns', '29db2920', !Select [3, !Split ['-', !Ref 'AWS::StackName']], '-', !Ref env]]]
209 | AssumeRolePolicyDocument:
210 | Version: "2012-10-17"
211 | Statement:
212 | - Sid: ""
213 | Effect: "Allow"
214 | Principal:
215 | Service: "cognito-idp.amazonaws.com"
216 | Action:
217 | - "sts:AssumeRole"
218 | Condition:
219 | StringEquals:
220 | sts:ExternalId: signal29db2920_role_external_id
221 | Policies:
222 | -
223 | PolicyName: signal29db2920-sns-policy
224 | PolicyDocument:
225 | Version: "2012-10-17"
226 | Statement:
227 | -
228 | Effect: "Allow"
229 | Action:
230 | - "sns:Publish"
231 | Resource: "*"
232 | # BEGIN USER POOL RESOURCES
233 | UserPool:
234 | # Created upon user selection
235 | # Depends on SNS Role for Arn if MFA is enabled
236 | Type: AWS::Cognito::UserPool
237 | UpdateReplacePolicy: Retain
238 | Properties:
239 | UserPoolName: !If [ShouldNotCreateEnvResources, !Ref userPoolName, !Join ['',[!Ref userPoolName, '-', !Ref env]]]
240 |
241 |
242 | UsernameConfiguration:
243 | CaseSensitive: false
244 |
245 | Schema:
246 |
247 | -
248 | Name: email
249 | Required: true
250 | Mutable: true
251 |
252 |
253 |
254 |
255 | AutoVerifiedAttributes:
256 |
257 | - email
258 |
259 |
260 |
261 | EmailVerificationMessage: !Ref emailVerificationMessage
262 | EmailVerificationSubject: !Ref emailVerificationSubject
263 |
264 | Policies:
265 | PasswordPolicy:
266 | MinimumLength: !Ref passwordPolicyMinLength
267 | RequireLowercase: false
268 | RequireNumbers: false
269 | RequireSymbols: false
270 | RequireUppercase: false
271 |
272 | UsernameAttributes: !Ref usernameAttributes
273 |
274 |
275 | MfaConfiguration: !Ref mfaConfiguration
276 | SmsVerificationMessage: !Ref smsVerificationMessage
277 | SmsAuthenticationMessage: !Ref smsAuthenticationMessage
278 | SmsConfiguration:
279 | SnsCallerArn: !GetAtt SNSRole.Arn
280 | ExternalId: signal29db2920_role_external_id
281 |
282 |
283 | UserPoolClientWeb:
284 | # Created provide application access to user pool
285 | # Depends on UserPool for ID reference
286 | Type: "AWS::Cognito::UserPoolClient"
287 | Properties:
288 | ClientName: signal29db2920_app_clientWeb
289 |
290 | RefreshTokenValidity: !Ref userpoolClientRefreshTokenValidity
291 | UserPoolId: !Ref UserPool
292 | DependsOn: UserPool
293 | UserPoolClient:
294 | # Created provide application access to user pool
295 | # Depends on UserPool for ID reference
296 | Type: "AWS::Cognito::UserPoolClient"
297 | Properties:
298 | ClientName: signal29db2920_app_client
299 |
300 | GenerateSecret: !Ref userpoolClientGenerateSecret
301 | RefreshTokenValidity: !Ref userpoolClientRefreshTokenValidity
302 | UserPoolId: !Ref UserPool
303 | DependsOn: UserPool
304 | # BEGIN USER POOL LAMBDA RESOURCES
305 | UserPoolClientRole:
306 | # Created to execute Lambda which gets userpool app client config values
307 | Type: 'AWS::IAM::Role'
308 | Properties:
309 | RoleName: !If [ShouldNotCreateEnvResources, !Ref userpoolClientLambdaRole, !Join ['',['upClientLambdaRole', '29db2920', !Select [3, !Split ['-', !Ref 'AWS::StackName']], '-', !Ref env]]]
310 | AssumeRolePolicyDocument:
311 | Version: '2012-10-17'
312 | Statement:
313 | - Effect: Allow
314 | Principal:
315 | Service:
316 | - lambda.amazonaws.com
317 | Action:
318 | - 'sts:AssumeRole'
319 | DependsOn: UserPoolClient
320 | UserPoolClientLambda:
321 | # Lambda which gets userpool app client config values
322 | # Depends on UserPool for id
323 | # Depends on UserPoolClientRole for role ARN
324 | Type: 'AWS::Lambda::Function'
325 | Properties:
326 | Code:
327 | ZipFile: !Join
328 | - |+
329 | - - 'const response = require(''cfn-response'');'
330 | - 'const aws = require(''aws-sdk'');'
331 | - 'const identity = new aws.CognitoIdentityServiceProvider();'
332 | - 'exports.handler = (event, context, callback) => {'
333 | - ' if (event.RequestType == ''Delete'') { '
334 | - ' response.send(event, context, response.SUCCESS, {})'
335 | - ' }'
336 | - ' if (event.RequestType == ''Update'' || event.RequestType == ''Create'') {'
337 | - ' const params = {'
338 | - ' ClientId: event.ResourceProperties.clientId,'
339 | - ' UserPoolId: event.ResourceProperties.userpoolId'
340 | - ' };'
341 | - ' identity.describeUserPoolClient(params).promise()'
342 | - ' .then((res) => {'
343 | - ' response.send(event, context, response.SUCCESS, {''appSecret'': res.UserPoolClient.ClientSecret});'
344 | - ' })'
345 | - ' .catch((err) => {'
346 | - ' response.send(event, context, response.FAILED, {err});'
347 | - ' });'
348 | - ' }'
349 | - '};'
350 | Handler: index.handler
351 | Runtime: nodejs12.x
352 | Timeout: 300
353 | Role: !GetAtt
354 | - UserPoolClientRole
355 | - Arn
356 | DependsOn: UserPoolClientRole
357 | UserPoolClientLambdaPolicy:
358 | # Sets userpool policy for the role that executes the Userpool Client Lambda
359 | # Depends on UserPool for Arn
360 | # Marked as depending on UserPoolClientRole for easier to understand CFN sequencing
361 | Type: 'AWS::IAM::Policy'
362 | Properties:
363 | PolicyName: signal29db2920_userpoolclient_lambda_iam_policy
364 | Roles:
365 | - !Ref UserPoolClientRole
366 | PolicyDocument:
367 | Version: '2012-10-17'
368 | Statement:
369 | - Effect: Allow
370 | Action:
371 | - 'cognito-idp:DescribeUserPoolClient'
372 | Resource: !GetAtt UserPool.Arn
373 | DependsOn: UserPoolClientLambda
374 | UserPoolClientLogPolicy:
375 | # Sets log policy for the role that executes the Userpool Client Lambda
376 | # Depends on UserPool for Arn
377 | # Marked as depending on UserPoolClientLambdaPolicy for easier to understand CFN sequencing
378 | Type: 'AWS::IAM::Policy'
379 | Properties:
380 | PolicyName: signal29db2920_userpoolclient_lambda_log_policy
381 | Roles:
382 | - !Ref UserPoolClientRole
383 | PolicyDocument:
384 | Version: 2012-10-17
385 | Statement:
386 | - Effect: Allow
387 | Action:
388 | - 'logs:CreateLogGroup'
389 | - 'logs:CreateLogStream'
390 | - 'logs:PutLogEvents'
391 | Resource: !Sub
392 | - arn:aws:logs:${region}:${account}:log-group:/aws/lambda/${lambda}:log-stream:*
393 | - { region: !Ref "AWS::Region", account: !Ref "AWS::AccountId", lambda: !Ref UserPoolClientLambda}
394 | DependsOn: UserPoolClientLambdaPolicy
395 | UserPoolClientInputs:
396 | # Values passed to Userpool client Lambda
397 | # Depends on UserPool for Id
398 | # Depends on UserPoolClient for Id
399 | # Marked as depending on UserPoolClientLambdaPolicy for easier to understand CFN sequencing
400 | Type: 'Custom::LambdaCallout'
401 | Properties:
402 | ServiceToken: !GetAtt UserPoolClientLambda.Arn
403 | clientId: !Ref UserPoolClient
404 | userpoolId: !Ref UserPool
405 | DependsOn: UserPoolClientLogPolicy
406 |
407 |
408 |
409 |
410 |
411 |
412 |
413 | # BEGIN IDENTITY POOL RESOURCES
414 |
415 |
416 | IdentityPool:
417 | # Always created
418 | Type: AWS::Cognito::IdentityPool
419 | Properties:
420 | IdentityPoolName: !If [ShouldNotCreateEnvResources, 'testAuthIdentityPool', !Join ['',['testAuthIdentityPool', '__', !Ref env]]]
421 |
422 | CognitoIdentityProviders:
423 | - ClientId: !Ref UserPoolClient
424 | ProviderName: !Sub
425 | - cognito-idp.${region}.amazonaws.com/${client}
426 | - { region: !Ref "AWS::Region", client: !Ref UserPool}
427 | - ClientId: !Ref UserPoolClientWeb
428 | ProviderName: !Sub
429 | - cognito-idp.${region}.amazonaws.com/${client}
430 | - { region: !Ref "AWS::Region", client: !Ref UserPool}
431 |
432 | AllowUnauthenticatedIdentities: !Ref allowUnauthenticatedIdentities
433 |
434 |
435 | DependsOn: UserPoolClientInputs
436 |
437 |
438 | IdentityPoolRoleMap:
439 | # Created to map Auth and Unauth roles to the identity pool
440 | # Depends on Identity Pool for ID ref
441 | Type: AWS::Cognito::IdentityPoolRoleAttachment
442 | Properties:
443 | IdentityPoolId: !Ref IdentityPool
444 | Roles:
445 | unauthenticated: !Ref unauthRoleArn
446 | authenticated: !Ref authRoleArn
447 | DependsOn: IdentityPool
448 |
449 |
450 | Outputs :
451 |
452 | IdentityPoolId:
453 | Value: !Ref 'IdentityPool'
454 | Description: Id for the identity pool
455 | IdentityPoolName:
456 | Value: !GetAtt IdentityPool.Name
457 |
458 |
459 |
460 |
461 | UserPoolId:
462 | Value: !Ref 'UserPool'
463 | Description: Id for the user pool
464 | UserPoolArn:
465 | Value: !GetAtt UserPool.Arn
466 | Description: Arn for the user pool
467 | UserPoolName:
468 | Value: !Ref userPoolName
469 | AppClientIDWeb:
470 | Value: !Ref 'UserPoolClientWeb'
471 | Description: The user pool app client id for web
472 | AppClientID:
473 | Value: !Ref 'UserPoolClient'
474 | Description: The user pool app client id
475 | AppClientSecret:
476 | Value: !GetAtt UserPoolClientInputs.appSecret
477 | Condition: ShouldOutputAppClientSecrets
478 |
479 |
480 |
481 |
482 |
483 |
484 |
485 |
486 |
--------------------------------------------------------------------------------
/amplify/backend/storage/s39381675c/s3-cloudformation-template.json:
--------------------------------------------------------------------------------
1 | {
2 | "AWSTemplateFormatVersion": "2010-09-09",
3 | "Description": "S3 resource stack creation using Amplify CLI",
4 | "Parameters": {
5 | "bucketName": {
6 | "Type": "String"
7 | },
8 | "authPolicyName": {
9 | "Type": "String"
10 | },
11 | "unauthPolicyName": {
12 | "Type": "String"
13 | },
14 | "authRoleName": {
15 | "Type": "String"
16 | },
17 | "unauthRoleName": {
18 | "Type": "String"
19 | },
20 | "s3PublicPolicy": {
21 | "Type": "String",
22 | "Default" : "NONE"
23 | },
24 | "s3PrivatePolicy": {
25 | "Type": "String",
26 | "Default" : "NONE"
27 | },
28 | "s3ProtectedPolicy": {
29 | "Type": "String",
30 | "Default" : "NONE"
31 | },
32 | "s3UploadsPolicy": {
33 | "Type": "String",
34 | "Default" : "NONE"
35 | },
36 | "s3ReadPolicy": {
37 | "Type": "String",
38 | "Default" : "NONE"
39 | },
40 | "s3PermissionsAuthenticatedPublic": {
41 | "Type": "String",
42 | "Default" : "DISALLOW"
43 | },
44 | "s3PermissionsAuthenticatedProtected": {
45 | "Type": "String",
46 | "Default" : "DISALLOW"
47 | },
48 | "s3PermissionsAuthenticatedPrivate": {
49 | "Type": "String",
50 | "Default" : "DISALLOW"
51 | },
52 | "s3PermissionsAuthenticatedUploads": {
53 | "Type": "String",
54 | "Default" : "DISALLOW"
55 | },
56 | "s3PermissionsGuestPublic": {
57 | "Type": "String",
58 | "Default" : "DISALLOW"
59 | },
60 | "s3PermissionsGuestUploads": {
61 | "Type": "String",
62 | "Default" : "DISALLOW" },
63 | "AuthenticatedAllowList": {
64 | "Type": "String",
65 | "Default" : "DISALLOW"
66 | },
67 | "GuestAllowList": {
68 | "Type": "String",
69 | "Default" : "DISALLOW"
70 | },
71 | "selectedGuestPermissions": {
72 | "Type": "CommaDelimitedList",
73 | "Default" : "NONE"
74 | },
75 | "selectedAuthenticatedPermissions": {
76 | "Type": "CommaDelimitedList",
77 | "Default" : "NONE"
78 | },
79 | "env": {
80 | "Type": "String"
81 | },
82 | "triggerFunction": {
83 | "Type": "String"
84 | }
85 |
86 |
87 | },
88 | "Conditions": {
89 | "ShouldNotCreateEnvResources": {
90 | "Fn::Equals": [
91 | {
92 | "Ref": "env"
93 | },
94 | "NONE"
95 | ]
96 | },
97 | "CreateAuthPublic": {
98 | "Fn::Not" : [{
99 | "Fn::Equals" : [
100 | {"Ref" : "s3PermissionsAuthenticatedPublic"},
101 | "DISALLOW"
102 | ]
103 | }]
104 | },
105 | "CreateAuthProtected": {
106 | "Fn::Not" : [{
107 | "Fn::Equals" : [
108 | {"Ref" : "s3PermissionsAuthenticatedProtected"},
109 | "DISALLOW"
110 | ]
111 | }]
112 | },
113 | "CreateAuthPrivate": {
114 | "Fn::Not" : [{
115 | "Fn::Equals" : [
116 | {"Ref" : "s3PermissionsAuthenticatedPrivate"},
117 | "DISALLOW"
118 | ]
119 | }]
120 | },
121 | "CreateAuthUploads": {
122 | "Fn::Not" : [{
123 | "Fn::Equals" : [
124 | {"Ref" : "s3PermissionsAuthenticatedUploads"},
125 | "DISALLOW"
126 | ]
127 | }]
128 | },
129 | "CreateGuestPublic": {
130 | "Fn::Not" : [{
131 | "Fn::Equals" : [
132 | {"Ref" : "s3PermissionsGuestPublic"},
133 | "DISALLOW"
134 | ]
135 | }]
136 | },
137 | "CreateGuestUploads": {
138 | "Fn::Not" : [{
139 | "Fn::Equals" : [
140 | {"Ref" : "s3PermissionsGuestUploads"},
141 | "DISALLOW"
142 | ]
143 | }]
144 | },
145 | "AuthReadAndList": {
146 | "Fn::Not" : [{
147 | "Fn::Equals" : [
148 | {"Ref" : "AuthenticatedAllowList"},
149 | "DISALLOW"
150 | ]
151 | }]
152 | },
153 | "GuestReadAndList": {
154 | "Fn::Not" : [{
155 | "Fn::Equals" : [
156 | {"Ref" : "GuestAllowList"},
157 | "DISALLOW"
158 | ]
159 | }]
160 | }
161 | },
162 | "Resources": {
163 | "S3Bucket": {
164 | "Type": "AWS::S3::Bucket",
165 |
166 | "DeletionPolicy" : "Retain",
167 | "Properties": {
168 | "BucketName": {
169 | "Fn::If": [
170 | "ShouldNotCreateEnvResources",
171 | {
172 | "Ref": "bucketName"
173 | },
174 | {
175 | "Fn::Join": [
176 | "",
177 | [
178 | {
179 | "Ref": "bucketName"
180 | },
181 | {
182 | "Fn::Select": [
183 | 3,
184 | {
185 | "Fn::Split": [
186 | "-",
187 | {
188 | "Ref": "AWS::StackName"
189 | }
190 | ]
191 | }
192 | ]
193 | },
194 | "-",
195 | {
196 | "Ref": "env"
197 | }
198 | ]
199 | ]
200 | }
201 | ]
202 | },
203 |
204 | "CorsConfiguration": {
205 | "CorsRules": [
206 | {
207 | "AllowedHeaders": [
208 | "*"
209 | ],
210 | "AllowedMethods": [
211 | "GET",
212 | "HEAD",
213 | "PUT",
214 | "POST",
215 | "DELETE"
216 | ],
217 | "AllowedOrigins": [
218 | "*"
219 | ],
220 | "ExposedHeaders": [
221 | "x-amz-server-side-encryption",
222 | "x-amz-request-id",
223 | "x-amz-id-2",
224 | "ETag"
225 | ],
226 | "Id": "S3CORSRuleId1",
227 | "MaxAge": "3000"
228 | }
229 | ]
230 | }
231 | }
232 | },
233 |
234 |
235 | "S3AuthPublicPolicy": {
236 | "DependsOn": [
237 | "S3Bucket"
238 | ],
239 | "Condition": "CreateAuthPublic",
240 | "Type": "AWS::IAM::Policy",
241 | "Properties": {
242 | "PolicyName": {
243 | "Ref": "s3PublicPolicy"
244 | },
245 | "Roles": [
246 | {
247 | "Ref": "authRoleName"
248 | }
249 | ],
250 | "PolicyDocument": {
251 | "Version": "2012-10-17",
252 | "Statement": [
253 | {
254 | "Effect": "Allow",
255 | "Action": {
256 | "Fn::Split" : [ "," , {
257 | "Ref": "s3PermissionsAuthenticatedPublic"
258 | } ]
259 | },
260 | "Resource": [
261 | {
262 | "Fn::Join": [
263 | "",
264 | [
265 | "arn:aws:s3:::",
266 | {
267 | "Ref": "S3Bucket"
268 | },
269 | "/public/*"
270 | ]
271 | ]
272 | }
273 | ]
274 | }
275 | ]
276 | }
277 | }
278 | },
279 | "S3AuthProtectedPolicy": {
280 | "DependsOn": [
281 | "S3Bucket"
282 | ],
283 | "Condition": "CreateAuthProtected",
284 | "Type": "AWS::IAM::Policy",
285 | "Properties": {
286 | "PolicyName": {
287 | "Ref": "s3ProtectedPolicy"
288 | },
289 | "Roles": [
290 | {
291 | "Ref": "authRoleName"
292 | }
293 | ],
294 | "PolicyDocument": {
295 | "Version": "2012-10-17",
296 | "Statement": [
297 | {
298 | "Effect": "Allow",
299 | "Action": {
300 | "Fn::Split" : [ "," , {
301 | "Ref": "s3PermissionsAuthenticatedProtected"
302 | } ]
303 | },
304 | "Resource": [
305 | {
306 | "Fn::Join": [
307 | "",
308 | [
309 | "arn:aws:s3:::",
310 | {
311 | "Ref": "S3Bucket"
312 | },
313 | "/protected/${cognito-identity.amazonaws.com:sub}/*"
314 | ]
315 | ]
316 | }
317 | ]
318 | }
319 | ]
320 | }
321 | }
322 | },
323 | "S3AuthPrivatePolicy": {
324 | "DependsOn": [
325 | "S3Bucket"
326 | ],
327 | "Condition": "CreateAuthPrivate",
328 | "Type": "AWS::IAM::Policy",
329 | "Properties": {
330 | "PolicyName": {
331 | "Ref": "s3PrivatePolicy"
332 | },
333 | "Roles": [
334 | {
335 | "Ref": "authRoleName"
336 | }
337 | ],
338 | "PolicyDocument": {
339 | "Version": "2012-10-17",
340 | "Statement": [
341 | {
342 | "Effect": "Allow",
343 | "Action": {
344 | "Fn::Split" : [ "," , {
345 | "Ref": "s3PermissionsAuthenticatedPrivate"
346 | } ]
347 | },
348 | "Resource": [
349 | {
350 | "Fn::Join": [
351 | "",
352 | [
353 | "arn:aws:s3:::",
354 | {
355 | "Ref": "S3Bucket"
356 | },
357 | "/private/${cognito-identity.amazonaws.com:sub}/*"
358 | ]
359 | ]
360 | }
361 | ]
362 | }
363 | ]
364 | }
365 | }
366 | },
367 | "S3AuthUploadPolicy": {
368 | "DependsOn": [
369 | "S3Bucket"
370 | ],
371 | "Condition": "CreateAuthUploads",
372 | "Type": "AWS::IAM::Policy",
373 | "Properties": {
374 | "PolicyName": {
375 | "Ref": "s3UploadsPolicy"
376 | },
377 | "Roles": [
378 | {
379 | "Ref": "authRoleName"
380 | }
381 | ],
382 | "PolicyDocument": {
383 | "Version": "2012-10-17",
384 | "Statement": [
385 | {
386 | "Effect": "Allow",
387 | "Action": {
388 | "Fn::Split" : [ "," , {
389 | "Ref": "s3PermissionsAuthenticatedUploads"
390 | } ]
391 | },
392 | "Resource": [
393 | {
394 | "Fn::Join": [
395 | "",
396 | [
397 | "arn:aws:s3:::",
398 | {
399 | "Ref": "S3Bucket"
400 | },
401 | "/uploads/*"
402 | ]
403 | ]
404 | }
405 | ]
406 | }
407 | ]
408 | }
409 | }
410 | },
411 | "S3AuthReadPolicy": {
412 | "DependsOn": [
413 | "S3Bucket"
414 | ],
415 | "Condition": "AuthReadAndList",
416 | "Type": "AWS::IAM::Policy",
417 | "Properties": {
418 | "PolicyName": {
419 | "Ref": "s3ReadPolicy"
420 | },
421 | "Roles": [
422 | {
423 | "Ref": "authRoleName"
424 | }
425 | ],
426 | "PolicyDocument": {
427 | "Version": "2012-10-17",
428 | "Statement": [
429 | {
430 | "Effect": "Allow",
431 | "Action": [
432 | "s3:GetObject"
433 | ],
434 | "Resource": [
435 | {
436 | "Fn::Join": [
437 | "",
438 | [
439 | "arn:aws:s3:::",
440 | {
441 | "Ref": "S3Bucket"
442 | },
443 | "/protected/*"
444 | ]
445 | ]
446 | }
447 | ]
448 | },
449 | {
450 | "Effect": "Allow",
451 | "Action": [
452 | "s3:ListBucket"
453 | ],
454 | "Resource": [
455 | {
456 | "Fn::Join": [
457 | "",
458 | [
459 | "arn:aws:s3:::",
460 | {
461 | "Ref": "S3Bucket"
462 | }
463 | ]
464 | ]
465 | }
466 | ],
467 | "Condition": {
468 | "StringLike": {
469 | "s3:prefix": [
470 | "public/",
471 | "public/*",
472 | "protected/",
473 | "protected/*",
474 | "private/${cognito-identity.amazonaws.com:sub}/",
475 | "private/${cognito-identity.amazonaws.com:sub}/*"
476 | ]
477 | }
478 | }
479 | }
480 | ]
481 | }
482 | }
483 | },
484 | "S3GuestPublicPolicy": {
485 | "DependsOn": [
486 | "S3Bucket"
487 | ],
488 | "Condition": "CreateGuestPublic",
489 | "Type": "AWS::IAM::Policy",
490 | "Properties": {
491 | "PolicyName": {
492 | "Ref": "s3PublicPolicy"
493 | },
494 | "Roles": [
495 | {
496 | "Ref": "unauthRoleName"
497 | }
498 | ],
499 | "PolicyDocument": {
500 | "Version": "2012-10-17",
501 | "Statement": [
502 | {
503 | "Effect": "Allow",
504 | "Action": {
505 | "Fn::Split" : [ "," , {
506 | "Ref": "s3PermissionsGuestPublic"
507 | } ]
508 | },
509 | "Resource": [
510 | {
511 | "Fn::Join": [
512 | "",
513 | [
514 | "arn:aws:s3:::",
515 | {
516 | "Ref": "S3Bucket"
517 | },
518 | "/public/*"
519 | ]
520 | ]
521 | }
522 | ]
523 | }
524 | ]
525 | }
526 | }
527 | },
528 | "S3GuestUploadPolicy": {
529 | "DependsOn": [
530 | "S3Bucket"
531 | ],
532 | "Condition": "CreateGuestUploads",
533 | "Type": "AWS::IAM::Policy",
534 | "Properties": {
535 | "PolicyName": {
536 | "Ref": "s3UploadsPolicy"
537 | },
538 | "Roles": [
539 | {
540 | "Ref": "unauthRoleName"
541 | }
542 | ],
543 | "PolicyDocument": {
544 | "Version": "2012-10-17",
545 | "Statement": [
546 | {
547 | "Effect": "Allow",
548 | "Action": {
549 | "Fn::Split" : [ "," , {
550 | "Ref": "s3PermissionsGuestUploads"
551 | } ]
552 | },
553 | "Resource": [
554 | {
555 | "Fn::Join": [
556 | "",
557 | [
558 | "arn:aws:s3:::",
559 | {
560 | "Ref": "S3Bucket"
561 | },
562 | "/uploads/*"
563 | ]
564 | ]
565 | }
566 | ]
567 | }
568 | ]
569 | }
570 | }
571 | },
572 | "S3GuestReadPolicy": {
573 | "DependsOn": [
574 | "S3Bucket"
575 | ],
576 | "Condition": "GuestReadAndList",
577 | "Type": "AWS::IAM::Policy",
578 | "Properties": {
579 | "PolicyName": {
580 | "Ref": "s3ReadPolicy"
581 | },
582 | "Roles": [
583 | {
584 | "Ref": "unauthRoleName"
585 | }
586 | ],
587 | "PolicyDocument": {
588 | "Version": "2012-10-17",
589 | "Statement": [
590 | {
591 | "Effect": "Allow",
592 | "Action": [
593 | "s3:GetObject"
594 | ],
595 | "Resource": [
596 | {
597 | "Fn::Join": [
598 | "",
599 | [
600 | "arn:aws:s3:::",
601 | {
602 | "Ref": "S3Bucket"
603 | },
604 | "/protected/*"
605 | ]
606 | ]
607 | }
608 | ]
609 | },
610 | {
611 | "Effect": "Allow",
612 | "Action": [
613 | "s3:ListBucket"
614 | ],
615 | "Resource": [
616 | {
617 | "Fn::Join": [
618 | "",
619 | [
620 | "arn:aws:s3:::",
621 | {
622 | "Ref": "S3Bucket"
623 | }
624 | ]
625 | ]
626 | }
627 | ],
628 | "Condition": {
629 | "StringLike": {
630 | "s3:prefix": [
631 | "public/",
632 | "public/*",
633 | "protected/",
634 | "protected/*"
635 | ]
636 | }
637 | }
638 | }
639 | ]
640 | }
641 | }
642 | }
643 | },
644 | "Outputs": {
645 | "BucketName": {
646 | "Value": {
647 | "Ref": "S3Bucket"
648 | },
649 | "Description": "Bucket name for the S3 bucket"
650 | },
651 | "Region": {
652 | "Value": {
653 | "Ref": "AWS::Region"
654 | }
655 | }
656 | }
657 | }
658 |
--------------------------------------------------------------------------------
/src/models/schema.js:
--------------------------------------------------------------------------------
1 | export const schema = {
2 | "models": {
3 | "Message": {
4 | "name": "Message",
5 | "fields": {
6 | "id": {
7 | "name": "id",
8 | "isArray": false,
9 | "type": "ID",
10 | "isRequired": true,
11 | "attributes": []
12 | },
13 | "content": {
14 | "name": "content",
15 | "isArray": false,
16 | "type": "String",
17 | "isRequired": false,
18 | "attributes": []
19 | },
20 | "userID": {
21 | "name": "userID",
22 | "isArray": false,
23 | "type": "ID",
24 | "isRequired": false,
25 | "attributes": []
26 | },
27 | "chatroomID": {
28 | "name": "chatroomID",
29 | "isArray": false,
30 | "type": "ID",
31 | "isRequired": false,
32 | "attributes": []
33 | },
34 | "image": {
35 | "name": "image",
36 | "isArray": false,
37 | "type": "String",
38 | "isRequired": false,
39 | "attributes": []
40 | },
41 | "audio": {
42 | "name": "audio",
43 | "isArray": false,
44 | "type": "String",
45 | "isRequired": false,
46 | "attributes": []
47 | },
48 | "status": {
49 | "name": "status",
50 | "isArray": false,
51 | "type": {
52 | "enum": "MessageStatus"
53 | },
54 | "isRequired": false,
55 | "attributes": []
56 | },
57 | "replyToMessageID": {
58 | "name": "replyToMessageID",
59 | "isArray": false,
60 | "type": "ID",
61 | "isRequired": false,
62 | "attributes": []
63 | },
64 | "forUserId": {
65 | "name": "forUserId",
66 | "isArray": false,
67 | "type": "ID",
68 | "isRequired": false,
69 | "attributes": []
70 | },
71 | "createdAt": {
72 | "name": "createdAt",
73 | "isArray": false,
74 | "type": "AWSDateTime",
75 | "isRequired": false,
76 | "attributes": [],
77 | "isReadOnly": true
78 | },
79 | "updatedAt": {
80 | "name": "updatedAt",
81 | "isArray": false,
82 | "type": "AWSDateTime",
83 | "isRequired": false,
84 | "attributes": [],
85 | "isReadOnly": true
86 | }
87 | },
88 | "syncable": true,
89 | "pluralName": "Messages",
90 | "attributes": [
91 | {
92 | "type": "model",
93 | "properties": {}
94 | },
95 | {
96 | "type": "key",
97 | "properties": {
98 | "name": "byUser",
99 | "fields": [
100 | "userID"
101 | ]
102 | }
103 | },
104 | {
105 | "type": "key",
106 | "properties": {
107 | "name": "byChatRoom",
108 | "fields": [
109 | "chatroomID"
110 | ]
111 | }
112 | },
113 | {
114 | "type": "auth",
115 | "properties": {
116 | "rules": [
117 | {
118 | "allow": "public",
119 | "operations": [
120 | "create",
121 | "update",
122 | "delete",
123 | "read"
124 | ]
125 | }
126 | ]
127 | }
128 | }
129 | ]
130 | },
131 | "ChatRoom": {
132 | "name": "ChatRoom",
133 | "fields": {
134 | "id": {
135 | "name": "id",
136 | "isArray": false,
137 | "type": "ID",
138 | "isRequired": true,
139 | "attributes": []
140 | },
141 | "newMessages": {
142 | "name": "newMessages",
143 | "isArray": false,
144 | "type": "Int",
145 | "isRequired": false,
146 | "attributes": []
147 | },
148 | "LastMessage": {
149 | "name": "LastMessage",
150 | "isArray": false,
151 | "type": {
152 | "model": "Message"
153 | },
154 | "isRequired": false,
155 | "attributes": [],
156 | "association": {
157 | "connectionType": "BELONGS_TO",
158 | "targetName": "chatRoomLastMessageId"
159 | }
160 | },
161 | "Messages": {
162 | "name": "Messages",
163 | "isArray": true,
164 | "type": {
165 | "model": "Message"
166 | },
167 | "isRequired": false,
168 | "attributes": [],
169 | "isArrayNullable": true,
170 | "association": {
171 | "connectionType": "HAS_MANY",
172 | "associatedWith": "chatroomID"
173 | }
174 | },
175 | "ChatRoomUsers": {
176 | "name": "ChatRoomUsers",
177 | "isArray": true,
178 | "type": {
179 | "model": "ChatRoomUser"
180 | },
181 | "isRequired": false,
182 | "attributes": [],
183 | "isArrayNullable": true,
184 | "association": {
185 | "connectionType": "HAS_MANY",
186 | "associatedWith": "chatroom"
187 | }
188 | },
189 | "Admin": {
190 | "name": "Admin",
191 | "isArray": false,
192 | "type": {
193 | "model": "User"
194 | },
195 | "isRequired": false,
196 | "attributes": [],
197 | "association": {
198 | "connectionType": "BELONGS_TO",
199 | "targetName": "chatRoomAdminId"
200 | }
201 | },
202 | "name": {
203 | "name": "name",
204 | "isArray": false,
205 | "type": "String",
206 | "isRequired": false,
207 | "attributes": []
208 | },
209 | "imageUri": {
210 | "name": "imageUri",
211 | "isArray": false,
212 | "type": "String",
213 | "isRequired": false,
214 | "attributes": []
215 | },
216 | "createdAt": {
217 | "name": "createdAt",
218 | "isArray": false,
219 | "type": "AWSDateTime",
220 | "isRequired": false,
221 | "attributes": [],
222 | "isReadOnly": true
223 | },
224 | "updatedAt": {
225 | "name": "updatedAt",
226 | "isArray": false,
227 | "type": "AWSDateTime",
228 | "isRequired": false,
229 | "attributes": [],
230 | "isReadOnly": true
231 | }
232 | },
233 | "syncable": true,
234 | "pluralName": "ChatRooms",
235 | "attributes": [
236 | {
237 | "type": "model",
238 | "properties": {}
239 | },
240 | {
241 | "type": "auth",
242 | "properties": {
243 | "rules": [
244 | {
245 | "allow": "public",
246 | "operations": [
247 | "create",
248 | "update",
249 | "delete",
250 | "read"
251 | ]
252 | }
253 | ]
254 | }
255 | }
256 | ]
257 | },
258 | "ChatRoomUser": {
259 | "name": "ChatRoomUser",
260 | "fields": {
261 | "id": {
262 | "name": "id",
263 | "isArray": false,
264 | "type": "ID",
265 | "isRequired": true,
266 | "attributes": []
267 | },
268 | "chatroom": {
269 | "name": "chatroom",
270 | "isArray": false,
271 | "type": {
272 | "model": "ChatRoom"
273 | },
274 | "isRequired": true,
275 | "attributes": [],
276 | "association": {
277 | "connectionType": "BELONGS_TO",
278 | "targetName": "chatroomID"
279 | }
280 | },
281 | "user": {
282 | "name": "user",
283 | "isArray": false,
284 | "type": {
285 | "model": "User"
286 | },
287 | "isRequired": true,
288 | "attributes": [],
289 | "association": {
290 | "connectionType": "BELONGS_TO",
291 | "targetName": "userID"
292 | }
293 | },
294 | "createdAt": {
295 | "name": "createdAt",
296 | "isArray": false,
297 | "type": "AWSDateTime",
298 | "isRequired": false,
299 | "attributes": [],
300 | "isReadOnly": true
301 | },
302 | "updatedAt": {
303 | "name": "updatedAt",
304 | "isArray": false,
305 | "type": "AWSDateTime",
306 | "isRequired": false,
307 | "attributes": [],
308 | "isReadOnly": true
309 | }
310 | },
311 | "syncable": true,
312 | "pluralName": "ChatRoomUsers",
313 | "attributes": [
314 | {
315 | "type": "model",
316 | "properties": {
317 | "queries": null
318 | }
319 | },
320 | {
321 | "type": "key",
322 | "properties": {
323 | "name": "byChatRoom",
324 | "fields": [
325 | "chatroomID",
326 | "userID"
327 | ]
328 | }
329 | },
330 | {
331 | "type": "key",
332 | "properties": {
333 | "name": "byUser",
334 | "fields": [
335 | "userID",
336 | "chatroomID"
337 | ]
338 | }
339 | },
340 | {
341 | "type": "auth",
342 | "properties": {
343 | "rules": [
344 | {
345 | "allow": "public",
346 | "operations": [
347 | "create",
348 | "update",
349 | "delete",
350 | "read"
351 | ]
352 | },
353 | {
354 | "allow": "public",
355 | "operations": [
356 | "create",
357 | "update",
358 | "delete",
359 | "read"
360 | ]
361 | }
362 | ]
363 | }
364 | }
365 | ]
366 | },
367 | "User": {
368 | "name": "User",
369 | "fields": {
370 | "id": {
371 | "name": "id",
372 | "isArray": false,
373 | "type": "ID",
374 | "isRequired": true,
375 | "attributes": []
376 | },
377 | "name": {
378 | "name": "name",
379 | "isArray": false,
380 | "type": "String",
381 | "isRequired": true,
382 | "attributes": []
383 | },
384 | "imageUri": {
385 | "name": "imageUri",
386 | "isArray": false,
387 | "type": "String",
388 | "isRequired": false,
389 | "attributes": []
390 | },
391 | "status": {
392 | "name": "status",
393 | "isArray": false,
394 | "type": "String",
395 | "isRequired": false,
396 | "attributes": []
397 | },
398 | "Messages": {
399 | "name": "Messages",
400 | "isArray": true,
401 | "type": {
402 | "model": "Message"
403 | },
404 | "isRequired": false,
405 | "attributes": [],
406 | "isArrayNullable": true,
407 | "association": {
408 | "connectionType": "HAS_MANY",
409 | "associatedWith": "userID"
410 | }
411 | },
412 | "chatrooms": {
413 | "name": "chatrooms",
414 | "isArray": true,
415 | "type": {
416 | "model": "ChatRoomUser"
417 | },
418 | "isRequired": false,
419 | "attributes": [],
420 | "isArrayNullable": true,
421 | "association": {
422 | "connectionType": "HAS_MANY",
423 | "associatedWith": "user"
424 | }
425 | },
426 | "lastOnlineAt": {
427 | "name": "lastOnlineAt",
428 | "isArray": false,
429 | "type": "AWSTimestamp",
430 | "isRequired": false,
431 | "attributes": []
432 | },
433 | "publicKey": {
434 | "name": "publicKey",
435 | "isArray": false,
436 | "type": "String",
437 | "isRequired": false,
438 | "attributes": []
439 | },
440 | "createdAt": {
441 | "name": "createdAt",
442 | "isArray": false,
443 | "type": "AWSDateTime",
444 | "isRequired": false,
445 | "attributes": [],
446 | "isReadOnly": true
447 | },
448 | "updatedAt": {
449 | "name": "updatedAt",
450 | "isArray": false,
451 | "type": "AWSDateTime",
452 | "isRequired": false,
453 | "attributes": [],
454 | "isReadOnly": true
455 | }
456 | },
457 | "syncable": true,
458 | "pluralName": "Users",
459 | "attributes": [
460 | {
461 | "type": "model",
462 | "properties": {}
463 | },
464 | {
465 | "type": "auth",
466 | "properties": {
467 | "rules": [
468 | {
469 | "allow": "public",
470 | "operations": [
471 | "create",
472 | "update",
473 | "delete",
474 | "read"
475 | ]
476 | }
477 | ]
478 | }
479 | }
480 | ]
481 | }
482 | },
483 | "enums": {
484 | "MessageStatus": {
485 | "name": "MessageStatus",
486 | "values": [
487 | "SENT",
488 | "DELIVERED",
489 | "READ"
490 | ]
491 | }
492 | },
493 | "nonModels": {},
494 | "version": "b79e86a6e122fd2d5af4a03d6f673ce3"
495 | };
--------------------------------------------------------------------------------