├── .eslintignore
├── .eslintrc.json
├── .gitignore
├── .prettierrc.json
├── .vscode
└── settings.json
├── .yarnrc.yml
├── App.tsx
├── Documents
└── Images
│ ├── Diagram.png
│ └── Screens.png
├── README.md
├── app.json
├── assets
├── adaptive-icon.png
├── favicon.png
├── fonts
│ ├── Pretendard-Bold.ttf
│ ├── Pretendard-ExtraBold.ttf
│ └── Pretendard-Regular.ttf
├── icon.png
├── icons
│ ├── ic_arrow_left.svg
│ ├── ic_arrow_left_white.svg
│ ├── ic_chat.svg
│ ├── ic_close.png
│ ├── ic_close.svg
│ ├── ic_send.svg
│ ├── ic_tab_archives.svg
│ ├── ic_tab_archives_active.svg
│ ├── ic_tab_home.svg
│ ├── ic_tab_home_active.svg
│ ├── ic_tab_settings.svg
│ └── ic_tab_settings_active.svg
├── images
│ ├── img_empty.png
│ └── thumbnail_placeholder_upload.png
├── langs
│ ├── en.json
│ └── ko.json
├── lotties
│ └── onboard.json
└── splash.png
├── babel.config.js
├── metro.config.js
├── package.json
├── src
├── components
│ ├── customAppbar
│ │ ├── index.tsx
│ │ └── styles.ts
│ ├── customHeader
│ │ ├── index.tsx
│ │ └── styles.ts
│ ├── divider
│ │ └── index.tsx
│ ├── emptyScreen
│ │ ├── index.tsx
│ │ └── styles.ts
│ ├── floatingButton
│ │ ├── index.tsx
│ │ └── styles.ts
│ └── keyboardAwareScrollView
│ │ ├── index.tsx
│ │ └── styles.ts
├── configs
│ ├── api_keys.ts
│ ├── recoil_keys.ts
│ ├── screen_types.ts
│ ├── socket_keys.ts
│ └── tasks_types.ts
├── dtos
│ ├── chat_dtos.ts
│ ├── location_dtos.ts
│ ├── post_dtos.ts
│ ├── socket_dtos.ts
│ └── upload_dtos.ts
├── helpers
│ └── device_utils.ts
├── hooks
│ ├── use_chat_socket.ts
│ └── use_location_socket.ts
├── i18n
│ ├── i18n.ts
│ ├── index.ts
│ └── translate.ts
├── main.tsx
├── navigation
│ ├── permissoned_navigator.tsx
│ ├── root_navigator.tsx
│ ├── tabs
│ │ ├── archives_navigator.tsx
│ │ ├── home_navigator.tsx
│ │ └── settings_navigator.tsx
│ └── un_permissoned_navigator.tsx
├── recoils
│ ├── location_states.ts
│ └── settings_states.ts
├── screens
│ ├── archives
│ │ ├── archiveItem
│ │ │ ├── index.tsx
│ │ │ └── styles.ts
│ │ ├── index.tsx
│ │ └── styles.ts
│ ├── home
│ │ ├── custom_map_styles.ts
│ │ ├── index.tsx
│ │ └── styles.ts
│ ├── infoDevelopers
│ │ ├── index.tsx
│ │ └── styles.ts
│ ├── onBoarding
│ │ ├── index.tsx
│ │ └── styles.ts
│ ├── postChat
│ │ ├── chatBalloon
│ │ │ ├── balloon_left.tsx
│ │ │ ├── balloon_right.tsx
│ │ │ ├── index.tsx
│ │ │ └── styles.ts
│ │ ├── index.tsx
│ │ ├── interfaces.ts
│ │ └── styles.ts
│ ├── postDetail
│ │ ├── index.tsx
│ │ ├── interfaces.ts
│ │ └── styles.ts
│ ├── postWrite
│ │ ├── index.tsx
│ │ ├── interfaces.ts
│ │ └── styles.ts
│ └── settings
│ │ ├── index.tsx
│ │ └── styles.ts
├── services
│ ├── posts_service.ts
│ └── upload_service.ts
├── themes.ts
├── types.d.ts
└── utils
│ └── recoil_utils.ts
└── tsconfig.json
/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | babel.config.js
3 | .expo/
4 | dist/
5 | npm-debug.*
6 | *.jks
7 | *.p8
8 | *.p12
9 | *.key
10 | *.mobileprovision
11 | *.orig.*
12 | web-build/
13 | web-report/
14 | metro.config.js
15 |
16 | # MacOS
17 | .DS_Store
18 |
19 | # Yarn
20 | yarn-error.log
21 | yarn.lock
22 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "root": true,
3 | "env": {
4 | "es6": true,
5 | "node": true
6 | },
7 | "parser": "@typescript-eslint/parser",
8 | "parserOptions": {
9 | "ecmaFeatures": {
10 | "jsx": true
11 | },
12 | "ecmaVersion": 2020,
13 | "sourceType": "module",
14 | "project": "./tsconfig.json"
15 | },
16 | "settings": {
17 | "import/parsers": {
18 | "@typescript-eslint/parser": [".ts", ".tsx"]
19 | },
20 | "import/resolver": {
21 | "typescript": {
22 | "alwaysTryTypes": true
23 | }
24 | }
25 | },
26 | "plugins": ["@typescript-eslint", "react", "react-hooks", "prettier"],
27 | "extends": [
28 | "airbnb",
29 | "airbnb/hooks",
30 | "airbnb-typescript",
31 | "plugin:import/recommended",
32 | "plugin:import/typescript",
33 | "plugin:jsx-a11y/recommended",
34 | "plugin:@typescript-eslint/recommended",
35 | "prettier",
36 | "plugin:prettier/recommended"
37 | ],
38 | "rules": {
39 | "prettier/prettier": "error",
40 | "import/prefer-default-export": "off",
41 | "import/no-unresolved": "off",
42 | "react/display-name": "off",
43 | "react/jsx-filename-extension": ["warn", {
44 | "extensions": [
45 | ".ts",
46 | ".tsx"
47 | ]
48 | }],
49 | "react/prop-types": "off",
50 | "react/react-in-jsx-scope": "off",
51 | "react/style-prop-object": "off",
52 | "react-hooks/exhaustive-deps": "off",
53 | "@typescript-eslint/ban-ts-comment": "off",
54 | "@typescript-eslint/ban-ts-ignore": "off",
55 | "@typescript-eslint/no-non-null-assertion": "off",
56 | "@typescript-eslint/return-await": "off"
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | .expo/
3 | dist/
4 | npm-debug.*
5 | *.jks
6 | *.p8
7 | *.p12
8 | *.key
9 | *.mobileprovision
10 | *.orig.*
11 | web-build/
12 |
13 | # macOS
14 | .DS_Store
15 |
16 | # yarn
17 | .yarn
18 | yarn.lock
19 |
20 |
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "trailingComma": "all",
3 | "arrowParens": "always",
4 | "singleQuote": true,
5 | "printWidth": 120,
6 | "semi": true,
7 | "useTabs": false
8 | }
9 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "eslint.validate": [
3 | "javascript",
4 | "javascriptreact",
5 | "typescript",
6 | "typescriptreact"
7 | ],
8 | "editor.codeActionsOnSave": {
9 | "source.fixAll": true
10 | },
11 | "[javascript]": {
12 | "editor.defaultFormatter": "esbenp.prettier-vscode"
13 | },
14 | "[javascriptreact]": {
15 | "editor.defaultFormatter": "esbenp.prettier-vscode"
16 | },
17 | "[typescript]": {
18 | "editor.defaultFormatter": "esbenp.prettier-vscode"
19 | },
20 | "[typescriptreact]": {
21 | "editor.defaultFormatter": "esbenp.prettier-vscode"
22 | },
23 | "javascript.preferences.importModuleSpecifier": "relative",
24 | "typescript.preferences.importModuleSpecifier": "relative",
25 | "prettier.configPath": ".prettierrc.json"
26 | }
27 |
--------------------------------------------------------------------------------
/.yarnrc.yml:
--------------------------------------------------------------------------------
1 | nodeLinker: node-modules
2 |
--------------------------------------------------------------------------------
/App.tsx:
--------------------------------------------------------------------------------
1 | import 'react-native-gesture-handler';
2 | import 'text-encoding';
3 | import { LogBox } from 'react-native';
4 | import App from './src/main';
5 |
6 | LogBox.ignoreLogs(['Setting a timer']);
7 |
8 | export default App;
9 |
--------------------------------------------------------------------------------
/Documents/Images/Diagram.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JaeWangL/blabla-mobile/c25785b8472241c91896c39eced27f6fdbade9db/Documents/Images/Diagram.png
--------------------------------------------------------------------------------
/Documents/Images/Screens.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JaeWangL/blabla-mobile/c25785b8472241c91896c39eced27f6fdbade9db/Documents/Images/Screens.png
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # BlaBla Mobile
2 |
3 | BlaBla is location-based social media service. This release intends to share a simplified version of Expo apps and Microservice architecture
4 |
5 | # BlaBla Repos
6 |
7 | For running this BlaBla mobile application, I built several back-end services with microservice architecture with Azure. You
8 | can find all BlaBla repos in the following locations:
9 |
10 | - [Mobile](https://github.com/JaeWangL/blabla-mobile)
11 | - [Service - Posts API](https://github.com/JaeWangL/blabla-api-posts)
12 | - [Service - Location API](https://github.com/JaeWangL/blabla-api-location)
13 | - [Service - Chat API](https://github.com/JaeWangL/blabla-api-chat)
14 | - [Service - Upload API](https://github.com/JaeWangL/blabla-api-upload)
15 | - [Infrastructure - Config](https://github.com/JaeWangL/blabla-infra-config)
16 | - [Infrastructure - Discovery](https://github.com/JaeWangL/blabla-infra-discovery)
17 |
18 | **Note:** This document is about the apps using **React Native Expo**.
19 |
20 | # Application Diagram
21 |
22 |
23 |
24 |
25 |
26 | # Application Screens
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "expo": {
3 | "name": "BlaBla",
4 | "slug": "BlaBla",
5 | "version": "1.0.1",
6 | "orientation": "portrait",
7 | "icon": "./assets/icon.png",
8 | "splash": {
9 | "image": "./assets/splash.png",
10 | "resizeMode": "contain",
11 | "backgroundColor": "#ffffff"
12 | },
13 | "updates": {
14 | "fallbackToCacheTimeout": 0
15 | },
16 | "assetBundlePatterns": [
17 | "**/*"
18 | ],
19 | "userInterfaceStyle": "automatic",
20 | "plugins": [
21 | [
22 | "expo-image-picker",
23 | {
24 | "photosPermission": "The app accesses your photos to let you share them with your friends."
25 | }
26 | ]
27 | ],
28 | "ios": {
29 | "bundleIdentifier": "com.hahahoho.blabla",
30 | "buildNumber": "1.0.0",
31 | "supportsTablet": true,
32 | "userInterfaceStyle": "light",
33 | "infoPlist": {
34 | "UIBackgroundModes": [
35 | "location",
36 | "fetch"
37 | ],
38 | "NSLocationAlwaysAndWhenInUseUsageDescription": "This app uses the location to track your location.",
39 | "NSLocationAlwaysUsageDescription": "This app uses the location to track your location.",
40 | "NSLocationWhenInUseUsageDescription": "This app uses the location to track your location.",
41 | "NSPhotoLibraryUsageDescription": "The app accesses your photos to let you share them with your friends.",
42 | "NSCameraUsageDescription": "Allow $(PRODUCT_NAME) to access your camera",
43 | "NSMicrophoneUsageDescription": "Allow $(PRODUCT_NAME) to access your microphone"
44 | },
45 | "config": {
46 | "googleMapsApiKey": "AIzaSyCAVnFWJrSnur-DoHtp0uRIux8Iv3kBSaM"
47 | }
48 | },
49 | "android": {
50 | "adaptiveIcon": {
51 | "foregroundImage": "./assets/adaptive-icon.png",
52 | "backgroundColor": "#FFFFFF"
53 | },
54 | "userInterfaceStyle": "dark",
55 | "permissions": [
56 | "ACCESS_COARSE_LOCATION",
57 | "ACCESS_FINE_LOCATION",
58 | "ACCESS_BACKGROUND_LOCATION",
59 | "android.permission.CAMERA",
60 | "android.permission.READ_EXTERNAL_STORAGE",
61 | "android.permission.WRITE_EXTERNAL_STORAGE",
62 | "android.permission.RECORD_AUDIO"
63 | ],
64 | "package": "com.hahahoho.blabla",
65 | "versionCode": 1,
66 | "config": {
67 | "googleMaps": {
68 | "apiKey": "AIzaSyCAVnFWJrSnur-DoHtp0uRIux8Iv3kBSaM"
69 | }
70 | }
71 | },
72 | "web": {
73 | "favicon": "./assets/favicon.png"
74 | },
75 | "packagerOpts": {
76 | "config": "metro.config.js"
77 | }
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/assets/adaptive-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JaeWangL/blabla-mobile/c25785b8472241c91896c39eced27f6fdbade9db/assets/adaptive-icon.png
--------------------------------------------------------------------------------
/assets/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JaeWangL/blabla-mobile/c25785b8472241c91896c39eced27f6fdbade9db/assets/favicon.png
--------------------------------------------------------------------------------
/assets/fonts/Pretendard-Bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JaeWangL/blabla-mobile/c25785b8472241c91896c39eced27f6fdbade9db/assets/fonts/Pretendard-Bold.ttf
--------------------------------------------------------------------------------
/assets/fonts/Pretendard-ExtraBold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JaeWangL/blabla-mobile/c25785b8472241c91896c39eced27f6fdbade9db/assets/fonts/Pretendard-ExtraBold.ttf
--------------------------------------------------------------------------------
/assets/fonts/Pretendard-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JaeWangL/blabla-mobile/c25785b8472241c91896c39eced27f6fdbade9db/assets/fonts/Pretendard-Regular.ttf
--------------------------------------------------------------------------------
/assets/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JaeWangL/blabla-mobile/c25785b8472241c91896c39eced27f6fdbade9db/assets/icon.png
--------------------------------------------------------------------------------
/assets/icons/ic_arrow_left.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/assets/icons/ic_arrow_left_white.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/assets/icons/ic_chat.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/assets/icons/ic_close.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JaeWangL/blabla-mobile/c25785b8472241c91896c39eced27f6fdbade9db/assets/icons/ic_close.png
--------------------------------------------------------------------------------
/assets/icons/ic_close.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/assets/icons/ic_send.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/assets/icons/ic_tab_archives.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/assets/icons/ic_tab_archives_active.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/assets/icons/ic_tab_home.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/assets/icons/ic_tab_home_active.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/assets/icons/ic_tab_settings.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/assets/icons/ic_tab_settings_active.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/assets/images/img_empty.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JaeWangL/blabla-mobile/c25785b8472241c91896c39eced27f6fdbade9db/assets/images/img_empty.png
--------------------------------------------------------------------------------
/assets/images/thumbnail_placeholder_upload.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JaeWangL/blabla-mobile/c25785b8472241c91896c39eced27f6fdbade9db/assets/images/thumbnail_placeholder_upload.png
--------------------------------------------------------------------------------
/assets/langs/en.json:
--------------------------------------------------------------------------------
1 | {
2 | "common": {
3 | "cancel": "Cancel",
4 | "noData": "There are no data to dispaly",
5 | "ok": "OK"
6 | },
7 | "button": {
8 | "acceptPermissionLocation": "Start Journey"
9 | },
10 | "dialogs": {
11 | "newPostTitle": "New post alert",
12 | "permissionDeniedTitle": "Permission Denied",
13 | "permissionRequestLocation": "You have to accept location permissions",
14 | "permissionRequestPhotos": "Please accept photos permissions",
15 | "postCreatingFailed": "Error is occured when writing post. Please try again later",
16 | "postCreatingFailedTitle": "Creation Failed",
17 | "postCreatingSucceed": "New post is created",
18 | "postCreatingSucceedTitle": "Post Created",
19 | "rateLimitedTitle": "Too many requests",
20 | "rateLimitedDesc": "Please try again later after {{retryRemainingS}}s",
21 | "uploadErrorTitle": "Upload Error",
22 | "uploadErrorExceed": "Maximum thumbnail size up to 5MB",
23 | "uploadErrorUnk": "Error is caused. Please try again later"
24 | },
25 | "placeholder": {
26 | "chatInput": "Please input messages ..."
27 | },
28 | "archives": {
29 | "title": "Archives"
30 | },
31 | "settings": {
32 | "groupInfo": "App Information",
33 | "infoDeveloper": "Developer Information",
34 | "infoVersion": "App Version",
35 | "title": "Settings"
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/assets/langs/ko.json:
--------------------------------------------------------------------------------
1 | {
2 | "common": {
3 | "cancel": "취소",
4 | "noData": "표시할 데이터가 없습니다.",
5 | "ok": "확인"
6 | },
7 | "button": {
8 | "acceptPermissionLocation": "동의하고 시작하기"
9 | },
10 | "dialogs": {
11 | "newPostTitle": "새로운 포스트 알림",
12 | "permissionDeniedTitle": "권한 에러",
13 | "permissionRequestLocation": "앱의 원활한 사용을 위해 위치정보 권한을 허용해 주세요.",
14 | "permissionRequestPhotos": "앱의 원활한 사용을 위해 사진 권한을 허용해 주세요.",
15 | "postCreatingFailed": "포스트를 작성하던 중 오류가 발생했습니다. 잠시 후 다시 시도해 주세요.",
16 | "postCreatingFailedTitle": "등록 에러",
17 | "postCreatingSucceed": "새로운 포스트가 등록되었습니다.",
18 | "postCreatingSucceedTitle": "등록 완료",
19 | "rateLimitedTitle": "연속클릭 알림",
20 | "rateLimitedDesc": "{{retryRemainingS}}초 후 다시 시도해 주세요.",
21 | "uploadErrorTitle": "업로드 오류",
22 | "uploadErrorExceed": "썸네일은 최대 5MB 까지 업로드 할 수 있습니다.",
23 | "uploadErrorUnk": "파일을 업로드 하던 중 오류가 발생했습니다. 잠시 후 다시 시도해 주세요."
24 | },
25 | "placeholder": {
26 | "chatInput": "메세지를 입력해 주세요 ..."
27 | },
28 | "archives": {
29 | "title": "모아보기"
30 | },
31 | "settings": {
32 | "groupInfo": "앱 정보",
33 | "infoDeveloper": "개발자 정보",
34 | "infoVersion": "앱 버전",
35 | "title": "설정"
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/assets/lotties/onboard.json:
--------------------------------------------------------------------------------
1 | {
2 | "v":"5.5.5",
3 | "fr":25,
4 | "ip":0,
5 | "op":20,
6 | "w":140,
7 | "h":140,
8 | "nm":"Top 1",
9 | "ddd":0,
10 | "assets":[
11 |
12 | ],
13 | "layers":[
14 | {
15 | "ddd":0,
16 | "ind":1,
17 | "ty":4,
18 | "nm":"1-1",
19 | "sr":1,
20 | "ks":{
21 | "o":{
22 | "a":1,
23 | "k":[
24 | {
25 | "i":{
26 | "x":[
27 | 0.833
28 | ],
29 | "y":[
30 | 0.833
31 | ]
32 | },
33 | "o":{
34 | "x":[
35 | 0.167
36 | ],
37 | "y":[
38 | 0.167
39 | ]
40 | },
41 | "t":0,
42 | "s":[
43 | 100
44 | ]
45 | },
46 | {
47 | "t":60,
48 | "s":[
49 | 0
50 | ]
51 | }
52 | ],
53 | "ix":11
54 | },
55 | "r":{
56 | "a":0,
57 | "k":0,
58 | "ix":10
59 | },
60 | "p":{
61 | "a":0,
62 | "k":[
63 | 70.678,
64 | 70.928,
65 | 0
66 | ],
67 | "ix":2
68 | },
69 | "a":{
70 | "a":0,
71 | "k":[
72 | 51.178,
73 | 214.428,
74 | 0
75 | ],
76 | "ix":1
77 | },
78 | "s":{
79 | "a":1,
80 | "k":[
81 | {
82 | "i":{
83 | "x":[
84 | 0.833,
85 | 0.833,
86 | 0.833
87 | ],
88 | "y":[
89 | 0.833,
90 | 0.833,
91 | 0.833
92 | ]
93 | },
94 | "o":{
95 | "x":[
96 | 0.167,
97 | 0.167,
98 | 0.167
99 | ],
100 | "y":[
101 | 0.167,
102 | 0.167,
103 | 0.167
104 | ]
105 | },
106 | "t":0,
107 | "s":[
108 | 100,
109 | 100,
110 | 100
111 | ]
112 | },
113 | {
114 | "i":{
115 | "x":[
116 | 0.833,
117 | 0.833,
118 | 0.833
119 | ],
120 | "y":[
121 | 0.833,
122 | 0.833,
123 | 0.833
124 | ]
125 | },
126 | "o":{
127 | "x":[
128 | 0.167,
129 | 0.167,
130 | 0.167
131 | ],
132 | "y":[
133 | 0.167,
134 | 0.167,
135 | 0.167
136 | ]
137 | },
138 | "t":20,
139 | "s":[
140 | 150,
141 | 150,
142 | 100
143 | ]
144 | },
145 | {
146 | "i":{
147 | "x":[
148 | 0.833,
149 | 0.833,
150 | 0.833
151 | ],
152 | "y":[
153 | 0.833,
154 | 0.833,
155 | 0.833
156 | ]
157 | },
158 | "o":{
159 | "x":[
160 | 0.167,
161 | 0.167,
162 | 0.167
163 | ],
164 | "y":[
165 | 0.167,
166 | 0.167,
167 | 0.167
168 | ]
169 | },
170 | "t":40,
171 | "s":[
172 | 200,
173 | 200,
174 | 100
175 | ]
176 | },
177 | {
178 | "t":60,
179 | "s":[
180 | 250,
181 | 250,
182 | 100
183 | ]
184 | }
185 | ],
186 | "ix":6
187 | }
188 | },
189 | "ao":0,
190 | "shapes":[
191 | {
192 | "ty":"gr",
193 | "it":[
194 | {
195 | "ind":0,
196 | "ty":"sh",
197 | "ix":1,
198 | "ks":{
199 | "a":0,
200 | "k":{
201 | "i":[
202 | [
203 | 13.767,
204 | 0
205 | ],
206 | [
207 | 0,
208 | -13.767
209 | ],
210 | [
211 | -13.767,
212 | 0
213 | ],
214 | [
215 | 0,
216 | 13.767
217 | ]
218 | ],
219 | "o":[
220 | [
221 | -13.767,
222 | 0
223 | ],
224 | [
225 | 0,
226 | 13.767
227 | ],
228 | [
229 | 13.767,
230 | 0
231 | ],
232 | [
233 | 0,
234 | -13.767
235 | ]
236 | ],
237 | "v":[
238 | [
239 | -6.25,
240 | -28.428
241 | ],
242 | [
243 | -31.178,
244 | -3.5
245 | ],
246 | [
247 | -6.25,
248 | 21.428
249 | ],
250 | [
251 | 18.678,
252 | -3.5
253 | ]
254 | ],
255 | "c":true
256 | },
257 | "ix":2
258 | },
259 | "nm":"路径 1",
260 | "mn":"ADBE Vector Shape - Group",
261 | "hd":false
262 | },
263 | {
264 | "ty":"st",
265 | "c":{
266 | "a":0,
267 | "k":[
268 | 0.29411764705882354,
269 | 0.2235294117647059,
270 | 0.9372549019607843,
271 | 1
272 | ],
273 | "ix":3
274 | },
275 | "o":{
276 | "a":0,
277 | "k":100,
278 | "ix":4
279 | },
280 | "w":{
281 | "a":1,
282 | "k":[
283 | {
284 | "i":{
285 | "x":[
286 | 0.833
287 | ],
288 | "y":[
289 | 0.833
290 | ]
291 | },
292 | "o":{
293 | "x":[
294 | 0.167
295 | ],
296 | "y":[
297 | 0.167
298 | ]
299 | },
300 | "t":0,
301 | "s":[
302 | 6
303 | ]
304 | },
305 | {
306 | "i":{
307 | "x":[
308 | 0.833
309 | ],
310 | "y":[
311 | 0.833
312 | ]
313 | },
314 | "o":{
315 | "x":[
316 | 0.167
317 | ],
318 | "y":[
319 | 0.167
320 | ]
321 | },
322 | "t":20,
323 | "s":[
324 | 5
325 | ]
326 | },
327 | {
328 | "i":{
329 | "x":[
330 | 0.833
331 | ],
332 | "y":[
333 | 0.833
334 | ]
335 | },
336 | "o":{
337 | "x":[
338 | 0.167
339 | ],
340 | "y":[
341 | 0.167
342 | ]
343 | },
344 | "t":40,
345 | "s":[
346 | 4
347 | ]
348 | },
349 | {
350 | "t":60,
351 | "s":[
352 | 3
353 | ]
354 | }
355 | ],
356 | "ix":5
357 | },
358 | "lc":1,
359 | "lj":1,
360 | "ml":4,
361 | "bm":0,
362 | "nm":"描边 1",
363 | "mn":"ADBE Vector Graphic - Stroke",
364 | "hd":false
365 | },
366 | {
367 | "ty":"tr",
368 | "p":{
369 | "a":0,
370 | "k":[
371 | 57.428,
372 | 217.928
373 | ],
374 | "ix":2
375 | },
376 | "a":{
377 | "a":0,
378 | "k":[
379 | 0,
380 | 0
381 | ],
382 | "ix":1
383 | },
384 | "s":{
385 | "a":0,
386 | "k":[
387 | 100,
388 | 100
389 | ],
390 | "ix":3
391 | },
392 | "r":{
393 | "a":0,
394 | "k":0,
395 | "ix":6
396 | },
397 | "o":{
398 | "a":0,
399 | "k":100,
400 | "ix":7
401 | },
402 | "sk":{
403 | "a":0,
404 | "k":0,
405 | "ix":4
406 | },
407 | "sa":{
408 | "a":0,
409 | "k":0,
410 | "ix":5
411 | },
412 | "nm":"变换"
413 | }
414 | ],
415 | "nm":"椭圆 1",
416 | "np":3,
417 | "cix":2,
418 | "bm":0,
419 | "ix":1,
420 | "mn":"ADBE Vector Group",
421 | "hd":false
422 | }
423 | ],
424 | "ip":0,
425 | "op":1526,
426 | "st":0,
427 | "bm":0
428 | },
429 | {
430 | "ddd":0,
431 | "ind":2,
432 | "ty":4,
433 | "nm":"1-2",
434 | "sr":1,
435 | "ks":{
436 | "o":{
437 | "a":1,
438 | "k":[
439 | {
440 | "i":{
441 | "x":[
442 | 0.833
443 | ],
444 | "y":[
445 | 0.833
446 | ]
447 | },
448 | "o":{
449 | "x":[
450 | 0.167
451 | ],
452 | "y":[
453 | 0.167
454 | ]
455 | },
456 | "t":20,
457 | "s":[
458 | 100
459 | ]
460 | },
461 | {
462 | "t":80,
463 | "s":[
464 | 0
465 | ]
466 | }
467 | ],
468 | "ix":11
469 | },
470 | "r":{
471 | "a":0,
472 | "k":0,
473 | "ix":10
474 | },
475 | "p":{
476 | "a":0,
477 | "k":[
478 | 613.678,
479 | 1432.428,
480 | 0
481 | ],
482 | "ix":2
483 | },
484 | "a":{
485 | "a":0,
486 | "k":[
487 | 51.178,
488 | 214.428,
489 | 0
490 | ],
491 | "ix":1
492 | },
493 | "s":{
494 | "a":1,
495 | "k":[
496 | {
497 | "i":{
498 | "x":[
499 | 0.833,
500 | 0.833,
501 | 0.833
502 | ],
503 | "y":[
504 | 0.833,
505 | 0.833,
506 | 0.833
507 | ]
508 | },
509 | "o":{
510 | "x":[
511 | 0.167,
512 | 0.167,
513 | 0.167
514 | ],
515 | "y":[
516 | 0.167,
517 | 0.167,
518 | 0.167
519 | ]
520 | },
521 | "t":20,
522 | "s":[
523 | 100,
524 | 100,
525 | 100
526 | ]
527 | },
528 | {
529 | "i":{
530 | "x":[
531 | 0.833,
532 | 0.833,
533 | 0.833
534 | ],
535 | "y":[
536 | 0.833,
537 | 0.833,
538 | 0.833
539 | ]
540 | },
541 | "o":{
542 | "x":[
543 | 0.167,
544 | 0.167,
545 | 0.167
546 | ],
547 | "y":[
548 | 0.167,
549 | 0.167,
550 | 0.167
551 | ]
552 | },
553 | "t":40,
554 | "s":[
555 | 150,
556 | 150,
557 | 100
558 | ]
559 | },
560 | {
561 | "i":{
562 | "x":[
563 | 0.833,
564 | 0.833,
565 | 0.833
566 | ],
567 | "y":[
568 | 0.833,
569 | 0.833,
570 | 0.833
571 | ]
572 | },
573 | "o":{
574 | "x":[
575 | 0.167,
576 | 0.167,
577 | 0.167
578 | ],
579 | "y":[
580 | 0.167,
581 | 0.167,
582 | 0.167
583 | ]
584 | },
585 | "t":60,
586 | "s":[
587 | 200,
588 | 200,
589 | 100
590 | ]
591 | },
592 | {
593 | "t":80,
594 | "s":[
595 | 250,
596 | 250,
597 | 100
598 | ]
599 | }
600 | ],
601 | "ix":6
602 | }
603 | },
604 | "ao":0,
605 | "shapes":[
606 | {
607 | "ty":"gr",
608 | "it":[
609 | {
610 | "ind":0,
611 | "ty":"sh",
612 | "ix":1,
613 | "ks":{
614 | "a":0,
615 | "k":{
616 | "i":[
617 | [
618 | 13.767,
619 | 0
620 | ],
621 | [
622 | 0,
623 | -13.767
624 | ],
625 | [
626 | -13.767,
627 | 0
628 | ],
629 | [
630 | 0,
631 | 13.767
632 | ]
633 | ],
634 | "o":[
635 | [
636 | -13.767,
637 | 0
638 | ],
639 | [
640 | 0,
641 | 13.767
642 | ],
643 | [
644 | 13.767,
645 | 0
646 | ],
647 | [
648 | 0,
649 | -13.767
650 | ]
651 | ],
652 | "v":[
653 | [
654 | -6.25,
655 | -28.428
656 | ],
657 | [
658 | -31.178,
659 | -3.5
660 | ],
661 | [
662 | -6.25,
663 | 21.428
664 | ],
665 | [
666 | 18.678,
667 | -3.5
668 | ]
669 | ],
670 | "c":true
671 | },
672 | "ix":2
673 | },
674 | "nm":"路径 1",
675 | "mn":"ADBE Vector Shape - Group",
676 | "hd":false
677 | },
678 | {
679 | "ty":"st",
680 | "c":{
681 | "a":0,
682 | "k":[
683 | 0.29411764705882354,
684 | 0.2235294117647059,
685 | 0.9372549019607843,
686 | 1
687 | ],
688 | "ix":3
689 | },
690 | "o":{
691 | "a":0,
692 | "k":100,
693 | "ix":4
694 | },
695 | "w":{
696 | "a":1,
697 | "k":[
698 | {
699 | "i":{
700 | "x":[
701 | 0.833
702 | ],
703 | "y":[
704 | 0.833
705 | ]
706 | },
707 | "o":{
708 | "x":[
709 | 0.167
710 | ],
711 | "y":[
712 | 0.167
713 | ]
714 | },
715 | "t":20,
716 | "s":[
717 | 6
718 | ]
719 | },
720 | {
721 | "i":{
722 | "x":[
723 | 0.833
724 | ],
725 | "y":[
726 | 0.833
727 | ]
728 | },
729 | "o":{
730 | "x":[
731 | 0.167
732 | ],
733 | "y":[
734 | 0.167
735 | ]
736 | },
737 | "t":40,
738 | "s":[
739 | 5
740 | ]
741 | },
742 | {
743 | "i":{
744 | "x":[
745 | 0.833
746 | ],
747 | "y":[
748 | 0.833
749 | ]
750 | },
751 | "o":{
752 | "x":[
753 | 0.167
754 | ],
755 | "y":[
756 | 0.167
757 | ]
758 | },
759 | "t":60,
760 | "s":[
761 | 4
762 | ]
763 | },
764 | {
765 | "t":80,
766 | "s":[
767 | 3
768 | ]
769 | }
770 | ],
771 | "ix":5
772 | },
773 | "lc":1,
774 | "lj":1,
775 | "ml":4,
776 | "bm":0,
777 | "nm":"描边 1",
778 | "mn":"ADBE Vector Graphic - Stroke",
779 | "hd":false
780 | },
781 | {
782 | "ty":"tr",
783 | "p":{
784 | "a":0,
785 | "k":[
786 | 57.428,
787 | 217.928
788 | ],
789 | "ix":2
790 | },
791 | "a":{
792 | "a":0,
793 | "k":[
794 | 0,
795 | 0
796 | ],
797 | "ix":1
798 | },
799 | "s":{
800 | "a":0,
801 | "k":[
802 | 100,
803 | 100
804 | ],
805 | "ix":3
806 | },
807 | "r":{
808 | "a":0,
809 | "k":0,
810 | "ix":6
811 | },
812 | "o":{
813 | "a":0,
814 | "k":100,
815 | "ix":7
816 | },
817 | "sk":{
818 | "a":0,
819 | "k":0,
820 | "ix":4
821 | },
822 | "sa":{
823 | "a":0,
824 | "k":0,
825 | "ix":5
826 | },
827 | "nm":"变换"
828 | }
829 | ],
830 | "nm":"椭圆 1",
831 | "np":3,
832 | "cix":2,
833 | "bm":0,
834 | "ix":1,
835 | "mn":"ADBE Vector Group",
836 | "hd":false
837 | }
838 | ],
839 | "ip":20,
840 | "op":1546,
841 | "st":20,
842 | "bm":0
843 | },
844 | {
845 | "ddd":0,
846 | "ind":3,
847 | "ty":4,
848 | "nm":"1-3",
849 | "sr":1,
850 | "ks":{
851 | "o":{
852 | "a":1,
853 | "k":[
854 | {
855 | "i":{
856 | "x":[
857 | 0.833
858 | ],
859 | "y":[
860 | 0.833
861 | ]
862 | },
863 | "o":{
864 | "x":[
865 | 0.167
866 | ],
867 | "y":[
868 | 0.167
869 | ]
870 | },
871 | "t":40,
872 | "s":[
873 | 100
874 | ]
875 | },
876 | {
877 | "t":100,
878 | "s":[
879 | 0
880 | ]
881 | }
882 | ],
883 | "ix":11
884 | },
885 | "r":{
886 | "a":0,
887 | "k":0,
888 | "ix":10
889 | },
890 | "p":{
891 | "a":0,
892 | "k":[
893 | 613.678,
894 | 1432.428,
895 | 0
896 | ],
897 | "ix":2
898 | },
899 | "a":{
900 | "a":0,
901 | "k":[
902 | 51.178,
903 | 214.428,
904 | 0
905 | ],
906 | "ix":1
907 | },
908 | "s":{
909 | "a":1,
910 | "k":[
911 | {
912 | "i":{
913 | "x":[
914 | 0.833,
915 | 0.833,
916 | 0.833
917 | ],
918 | "y":[
919 | 0.833,
920 | 0.833,
921 | 0.833
922 | ]
923 | },
924 | "o":{
925 | "x":[
926 | 0.167,
927 | 0.167,
928 | 0.167
929 | ],
930 | "y":[
931 | 0.167,
932 | 0.167,
933 | 0.167
934 | ]
935 | },
936 | "t":40,
937 | "s":[
938 | 100,
939 | 100,
940 | 100
941 | ]
942 | },
943 | {
944 | "i":{
945 | "x":[
946 | 0.833,
947 | 0.833,
948 | 0.833
949 | ],
950 | "y":[
951 | 0.833,
952 | 0.833,
953 | 0.833
954 | ]
955 | },
956 | "o":{
957 | "x":[
958 | 0.167,
959 | 0.167,
960 | 0.167
961 | ],
962 | "y":[
963 | 0.167,
964 | 0.167,
965 | 0.167
966 | ]
967 | },
968 | "t":60,
969 | "s":[
970 | 150,
971 | 150,
972 | 100
973 | ]
974 | },
975 | {
976 | "i":{
977 | "x":[
978 | 0.833,
979 | 0.833,
980 | 0.833
981 | ],
982 | "y":[
983 | 0.833,
984 | 0.833,
985 | 0.833
986 | ]
987 | },
988 | "o":{
989 | "x":[
990 | 0.167,
991 | 0.167,
992 | 0.167
993 | ],
994 | "y":[
995 | 0.167,
996 | 0.167,
997 | 0.167
998 | ]
999 | },
1000 | "t":80,
1001 | "s":[
1002 | 200,
1003 | 200,
1004 | 100
1005 | ]
1006 | },
1007 | {
1008 | "t":100,
1009 | "s":[
1010 | 250,
1011 | 250,
1012 | 100
1013 | ]
1014 | }
1015 | ],
1016 | "ix":6
1017 | }
1018 | },
1019 | "ao":0,
1020 | "shapes":[
1021 | {
1022 | "ty":"gr",
1023 | "it":[
1024 | {
1025 | "ind":0,
1026 | "ty":"sh",
1027 | "ix":1,
1028 | "ks":{
1029 | "a":0,
1030 | "k":{
1031 | "i":[
1032 | [
1033 | 13.767,
1034 | 0
1035 | ],
1036 | [
1037 | 0,
1038 | -13.767
1039 | ],
1040 | [
1041 | -13.767,
1042 | 0
1043 | ],
1044 | [
1045 | 0,
1046 | 13.767
1047 | ]
1048 | ],
1049 | "o":[
1050 | [
1051 | -13.767,
1052 | 0
1053 | ],
1054 | [
1055 | 0,
1056 | 13.767
1057 | ],
1058 | [
1059 | 13.767,
1060 | 0
1061 | ],
1062 | [
1063 | 0,
1064 | -13.767
1065 | ]
1066 | ],
1067 | "v":[
1068 | [
1069 | -6.25,
1070 | -28.428
1071 | ],
1072 | [
1073 | -31.178,
1074 | -3.5
1075 | ],
1076 | [
1077 | -6.25,
1078 | 21.428
1079 | ],
1080 | [
1081 | 18.678,
1082 | -3.5
1083 | ]
1084 | ],
1085 | "c":true
1086 | },
1087 | "ix":2
1088 | },
1089 | "nm":"路径 1",
1090 | "mn":"ADBE Vector Shape - Group",
1091 | "hd":false
1092 | },
1093 | {
1094 | "ty":"st",
1095 | "c":{
1096 | "a":0,
1097 | "k":[
1098 | 0.29411764705882354,
1099 | 0.2235294117647059,
1100 | 0.9372549019607843,
1101 | 1
1102 | ],
1103 | "ix":3
1104 | },
1105 | "o":{
1106 | "a":0,
1107 | "k":100,
1108 | "ix":4
1109 | },
1110 | "w":{
1111 | "a":1,
1112 | "k":[
1113 | {
1114 | "i":{
1115 | "x":[
1116 | 0.833
1117 | ],
1118 | "y":[
1119 | 0.833
1120 | ]
1121 | },
1122 | "o":{
1123 | "x":[
1124 | 0.167
1125 | ],
1126 | "y":[
1127 | 0.167
1128 | ]
1129 | },
1130 | "t":40,
1131 | "s":[
1132 | 6
1133 | ]
1134 | },
1135 | {
1136 | "i":{
1137 | "x":[
1138 | 0.833
1139 | ],
1140 | "y":[
1141 | 0.833
1142 | ]
1143 | },
1144 | "o":{
1145 | "x":[
1146 | 0.167
1147 | ],
1148 | "y":[
1149 | 0.167
1150 | ]
1151 | },
1152 | "t":60,
1153 | "s":[
1154 | 5
1155 | ]
1156 | },
1157 | {
1158 | "i":{
1159 | "x":[
1160 | 0.833
1161 | ],
1162 | "y":[
1163 | 0.833
1164 | ]
1165 | },
1166 | "o":{
1167 | "x":[
1168 | 0.167
1169 | ],
1170 | "y":[
1171 | 0.167
1172 | ]
1173 | },
1174 | "t":80,
1175 | "s":[
1176 | 4
1177 | ]
1178 | },
1179 | {
1180 | "t":100,
1181 | "s":[
1182 | 3
1183 | ]
1184 | }
1185 | ],
1186 | "ix":5
1187 | },
1188 | "lc":1,
1189 | "lj":1,
1190 | "ml":4,
1191 | "bm":0,
1192 | "nm":"描边 1",
1193 | "mn":"ADBE Vector Graphic - Stroke",
1194 | "hd":false
1195 | },
1196 | {
1197 | "ty":"tr",
1198 | "p":{
1199 | "a":0,
1200 | "k":[
1201 | 57.428,
1202 | 217.928
1203 | ],
1204 | "ix":2
1205 | },
1206 | "a":{
1207 | "a":0,
1208 | "k":[
1209 | 0,
1210 | 0
1211 | ],
1212 | "ix":1
1213 | },
1214 | "s":{
1215 | "a":0,
1216 | "k":[
1217 | 100,
1218 | 100
1219 | ],
1220 | "ix":3
1221 | },
1222 | "r":{
1223 | "a":0,
1224 | "k":0,
1225 | "ix":6
1226 | },
1227 | "o":{
1228 | "a":0,
1229 | "k":100,
1230 | "ix":7
1231 | },
1232 | "sk":{
1233 | "a":0,
1234 | "k":0,
1235 | "ix":4
1236 | },
1237 | "sa":{
1238 | "a":0,
1239 | "k":0,
1240 | "ix":5
1241 | },
1242 | "nm":"变换"
1243 | }
1244 | ],
1245 | "nm":"椭圆 1",
1246 | "np":3,
1247 | "cix":2,
1248 | "bm":0,
1249 | "ix":1,
1250 | "mn":"ADBE Vector Group",
1251 | "hd":false
1252 | }
1253 | ],
1254 | "ip":40,
1255 | "op":1566,
1256 | "st":40,
1257 | "bm":0
1258 | },
1259 | {
1260 | "ddd":0,
1261 | "ind":4,
1262 | "ty":4,
1263 | "nm":"1-4",
1264 | "sr":1,
1265 | "ks":{
1266 | "o":{
1267 | "a":1,
1268 | "k":[
1269 | {
1270 | "i":{
1271 | "x":[
1272 | 0.833
1273 | ],
1274 | "y":[
1275 | 0.833
1276 | ]
1277 | },
1278 | "o":{
1279 | "x":[
1280 | 0.167
1281 | ],
1282 | "y":[
1283 | 0.167
1284 | ]
1285 | },
1286 | "t":-20,
1287 | "s":[
1288 | 100
1289 | ]
1290 | },
1291 | {
1292 | "t":40,
1293 | "s":[
1294 | 0
1295 | ]
1296 | }
1297 | ],
1298 | "ix":11
1299 | },
1300 | "r":{
1301 | "a":0,
1302 | "k":0,
1303 | "ix":10
1304 | },
1305 | "p":{
1306 | "a":0,
1307 | "k":[
1308 | 70.678,
1309 | 70.928,
1310 | 0
1311 | ],
1312 | "ix":2
1313 | },
1314 | "a":{
1315 | "a":0,
1316 | "k":[
1317 | 51.178,
1318 | 214.428,
1319 | 0
1320 | ],
1321 | "ix":1
1322 | },
1323 | "s":{
1324 | "a":1,
1325 | "k":[
1326 | {
1327 | "i":{
1328 | "x":[
1329 | 0.833,
1330 | 0.833,
1331 | 0.833
1332 | ],
1333 | "y":[
1334 | 0.833,
1335 | 0.833,
1336 | 0.833
1337 | ]
1338 | },
1339 | "o":{
1340 | "x":[
1341 | 0.167,
1342 | 0.167,
1343 | 0.167
1344 | ],
1345 | "y":[
1346 | 0.167,
1347 | 0.167,
1348 | 0.167
1349 | ]
1350 | },
1351 | "t":-20,
1352 | "s":[
1353 | 100,
1354 | 100,
1355 | 100
1356 | ]
1357 | },
1358 | {
1359 | "i":{
1360 | "x":[
1361 | 0.833,
1362 | 0.833,
1363 | 0.833
1364 | ],
1365 | "y":[
1366 | 0.833,
1367 | 0.833,
1368 | 0.833
1369 | ]
1370 | },
1371 | "o":{
1372 | "x":[
1373 | 0.167,
1374 | 0.167,
1375 | 0.167
1376 | ],
1377 | "y":[
1378 | 0.167,
1379 | 0.167,
1380 | 0.167
1381 | ]
1382 | },
1383 | "t":0,
1384 | "s":[
1385 | 150,
1386 | 150,
1387 | 100
1388 | ]
1389 | },
1390 | {
1391 | "i":{
1392 | "x":[
1393 | 0.833,
1394 | 0.833,
1395 | 0.833
1396 | ],
1397 | "y":[
1398 | 0.833,
1399 | 0.833,
1400 | 0.833
1401 | ]
1402 | },
1403 | "o":{
1404 | "x":[
1405 | 0.167,
1406 | 0.167,
1407 | 0.167
1408 | ],
1409 | "y":[
1410 | 0.167,
1411 | 0.167,
1412 | 0.167
1413 | ]
1414 | },
1415 | "t":20,
1416 | "s":[
1417 | 200,
1418 | 200,
1419 | 100
1420 | ]
1421 | },
1422 | {
1423 | "t":40,
1424 | "s":[
1425 | 250,
1426 | 250,
1427 | 100
1428 | ]
1429 | }
1430 | ],
1431 | "ix":6
1432 | }
1433 | },
1434 | "ao":0,
1435 | "shapes":[
1436 | {
1437 | "ty":"gr",
1438 | "it":[
1439 | {
1440 | "ind":0,
1441 | "ty":"sh",
1442 | "ix":1,
1443 | "ks":{
1444 | "a":0,
1445 | "k":{
1446 | "i":[
1447 | [
1448 | 13.767,
1449 | 0
1450 | ],
1451 | [
1452 | 0,
1453 | -13.767
1454 | ],
1455 | [
1456 | -13.767,
1457 | 0
1458 | ],
1459 | [
1460 | 0,
1461 | 13.767
1462 | ]
1463 | ],
1464 | "o":[
1465 | [
1466 | -13.767,
1467 | 0
1468 | ],
1469 | [
1470 | 0,
1471 | 13.767
1472 | ],
1473 | [
1474 | 13.767,
1475 | 0
1476 | ],
1477 | [
1478 | 0,
1479 | -13.767
1480 | ]
1481 | ],
1482 | "v":[
1483 | [
1484 | -6.25,
1485 | -28.428
1486 | ],
1487 | [
1488 | -31.178,
1489 | -3.5
1490 | ],
1491 | [
1492 | -6.25,
1493 | 21.428
1494 | ],
1495 | [
1496 | 18.678,
1497 | -3.5
1498 | ]
1499 | ],
1500 | "c":true
1501 | },
1502 | "ix":2
1503 | },
1504 | "nm":"路径 1",
1505 | "mn":"ADBE Vector Shape - Group",
1506 | "hd":false
1507 | },
1508 | {
1509 | "ty":"st",
1510 | "c":{
1511 | "a":0,
1512 | "k":[
1513 | 0.29411764705882354,
1514 | 0.2235294117647059,
1515 | 0.9372549019607843,
1516 | 1
1517 | ],
1518 | "ix":3
1519 | },
1520 | "o":{
1521 | "a":0,
1522 | "k":100,
1523 | "ix":4
1524 | },
1525 | "w":{
1526 | "a":1,
1527 | "k":[
1528 | {
1529 | "i":{
1530 | "x":[
1531 | 0.833
1532 | ],
1533 | "y":[
1534 | 0.833
1535 | ]
1536 | },
1537 | "o":{
1538 | "x":[
1539 | 0.167
1540 | ],
1541 | "y":[
1542 | 0.167
1543 | ]
1544 | },
1545 | "t":-20,
1546 | "s":[
1547 | 6
1548 | ]
1549 | },
1550 | {
1551 | "i":{
1552 | "x":[
1553 | 0.833
1554 | ],
1555 | "y":[
1556 | 0.833
1557 | ]
1558 | },
1559 | "o":{
1560 | "x":[
1561 | 0.167
1562 | ],
1563 | "y":[
1564 | 0.167
1565 | ]
1566 | },
1567 | "t":0,
1568 | "s":[
1569 | 5
1570 | ]
1571 | },
1572 | {
1573 | "i":{
1574 | "x":[
1575 | 0.833
1576 | ],
1577 | "y":[
1578 | 0.833
1579 | ]
1580 | },
1581 | "o":{
1582 | "x":[
1583 | 0.167
1584 | ],
1585 | "y":[
1586 | 0.167
1587 | ]
1588 | },
1589 | "t":20,
1590 | "s":[
1591 | 4
1592 | ]
1593 | },
1594 | {
1595 | "t":40,
1596 | "s":[
1597 | 3
1598 | ]
1599 | }
1600 | ],
1601 | "ix":5
1602 | },
1603 | "lc":1,
1604 | "lj":1,
1605 | "ml":4,
1606 | "bm":0,
1607 | "nm":"描边 1",
1608 | "mn":"ADBE Vector Graphic - Stroke",
1609 | "hd":false
1610 | },
1611 | {
1612 | "ty":"tr",
1613 | "p":{
1614 | "a":0,
1615 | "k":[
1616 | 57.428,
1617 | 217.928
1618 | ],
1619 | "ix":2
1620 | },
1621 | "a":{
1622 | "a":0,
1623 | "k":[
1624 | 0,
1625 | 0
1626 | ],
1627 | "ix":1
1628 | },
1629 | "s":{
1630 | "a":0,
1631 | "k":[
1632 | 100,
1633 | 100
1634 | ],
1635 | "ix":3
1636 | },
1637 | "r":{
1638 | "a":0,
1639 | "k":0,
1640 | "ix":6
1641 | },
1642 | "o":{
1643 | "a":0,
1644 | "k":100,
1645 | "ix":7
1646 | },
1647 | "sk":{
1648 | "a":0,
1649 | "k":0,
1650 | "ix":4
1651 | },
1652 | "sa":{
1653 | "a":0,
1654 | "k":0,
1655 | "ix":5
1656 | },
1657 | "nm":"变换"
1658 | }
1659 | ],
1660 | "nm":"椭圆 1",
1661 | "np":3,
1662 | "cix":2,
1663 | "bm":0,
1664 | "ix":1,
1665 | "mn":"ADBE Vector Group",
1666 | "hd":false
1667 | }
1668 | ],
1669 | "ip":-20,
1670 | "op":1506,
1671 | "st":-20,
1672 | "bm":0
1673 | },
1674 | {
1675 | "ddd":0,
1676 | "ind":5,
1677 | "ty":4,
1678 | "nm":"1-5",
1679 | "sr":1,
1680 | "ks":{
1681 | "o":{
1682 | "a":1,
1683 | "k":[
1684 | {
1685 | "i":{
1686 | "x":[
1687 | 0.833
1688 | ],
1689 | "y":[
1690 | 0.833
1691 | ]
1692 | },
1693 | "o":{
1694 | "x":[
1695 | 0.167
1696 | ],
1697 | "y":[
1698 | 0.167
1699 | ]
1700 | },
1701 | "t":-40,
1702 | "s":[
1703 | 100
1704 | ]
1705 | },
1706 | {
1707 | "t":20,
1708 | "s":[
1709 | 0
1710 | ]
1711 | }
1712 | ],
1713 | "ix":11
1714 | },
1715 | "r":{
1716 | "a":0,
1717 | "k":0,
1718 | "ix":10
1719 | },
1720 | "p":{
1721 | "a":0,
1722 | "k":[
1723 | 70.678,
1724 | 70.928,
1725 | 0
1726 | ],
1727 | "ix":2
1728 | },
1729 | "a":{
1730 | "a":0,
1731 | "k":[
1732 | 51.178,
1733 | 214.428,
1734 | 0
1735 | ],
1736 | "ix":1
1737 | },
1738 | "s":{
1739 | "a":1,
1740 | "k":[
1741 | {
1742 | "i":{
1743 | "x":[
1744 | 0.833,
1745 | 0.833,
1746 | 0.833
1747 | ],
1748 | "y":[
1749 | 0.833,
1750 | 0.833,
1751 | 0.833
1752 | ]
1753 | },
1754 | "o":{
1755 | "x":[
1756 | 0.167,
1757 | 0.167,
1758 | 0.167
1759 | ],
1760 | "y":[
1761 | 0.167,
1762 | 0.167,
1763 | 0.167
1764 | ]
1765 | },
1766 | "t":-40,
1767 | "s":[
1768 | 100,
1769 | 100,
1770 | 100
1771 | ]
1772 | },
1773 | {
1774 | "i":{
1775 | "x":[
1776 | 0.833,
1777 | 0.833,
1778 | 0.833
1779 | ],
1780 | "y":[
1781 | 0.833,
1782 | 0.833,
1783 | 0.833
1784 | ]
1785 | },
1786 | "o":{
1787 | "x":[
1788 | 0.167,
1789 | 0.167,
1790 | 0.167
1791 | ],
1792 | "y":[
1793 | 0.167,
1794 | 0.167,
1795 | 0.167
1796 | ]
1797 | },
1798 | "t":-20,
1799 | "s":[
1800 | 150,
1801 | 150,
1802 | 100
1803 | ]
1804 | },
1805 | {
1806 | "i":{
1807 | "x":[
1808 | 0.833,
1809 | 0.833,
1810 | 0.833
1811 | ],
1812 | "y":[
1813 | 0.833,
1814 | 0.833,
1815 | 0.833
1816 | ]
1817 | },
1818 | "o":{
1819 | "x":[
1820 | 0.167,
1821 | 0.167,
1822 | 0.167
1823 | ],
1824 | "y":[
1825 | 0.167,
1826 | 0.167,
1827 | 0.167
1828 | ]
1829 | },
1830 | "t":0,
1831 | "s":[
1832 | 200,
1833 | 200,
1834 | 100
1835 | ]
1836 | },
1837 | {
1838 | "t":20,
1839 | "s":[
1840 | 250,
1841 | 250,
1842 | 100
1843 | ]
1844 | }
1845 | ],
1846 | "ix":6
1847 | }
1848 | },
1849 | "ao":0,
1850 | "shapes":[
1851 | {
1852 | "ty":"gr",
1853 | "it":[
1854 | {
1855 | "ind":0,
1856 | "ty":"sh",
1857 | "ix":1,
1858 | "ks":{
1859 | "a":0,
1860 | "k":{
1861 | "i":[
1862 | [
1863 | 13.767,
1864 | 0
1865 | ],
1866 | [
1867 | 0,
1868 | -13.767
1869 | ],
1870 | [
1871 | -13.767,
1872 | 0
1873 | ],
1874 | [
1875 | 0,
1876 | 13.767
1877 | ]
1878 | ],
1879 | "o":[
1880 | [
1881 | -13.767,
1882 | 0
1883 | ],
1884 | [
1885 | 0,
1886 | 13.767
1887 | ],
1888 | [
1889 | 13.767,
1890 | 0
1891 | ],
1892 | [
1893 | 0,
1894 | -13.767
1895 | ]
1896 | ],
1897 | "v":[
1898 | [
1899 | -6.25,
1900 | -28.428
1901 | ],
1902 | [
1903 | -31.178,
1904 | -3.5
1905 | ],
1906 | [
1907 | -6.25,
1908 | 21.428
1909 | ],
1910 | [
1911 | 18.678,
1912 | -3.5
1913 | ]
1914 | ],
1915 | "c":true
1916 | },
1917 | "ix":2
1918 | },
1919 | "nm":"路径 1",
1920 | "mn":"ADBE Vector Shape - Group",
1921 | "hd":false
1922 | },
1923 | {
1924 | "ty":"st",
1925 | "c":{
1926 | "a":0,
1927 | "k":[
1928 | 0.29411764705882354,
1929 | 0.2235294117647059,
1930 | 0.9372549019607843,
1931 | 1
1932 | ],
1933 | "ix":3
1934 | },
1935 | "o":{
1936 | "a":0,
1937 | "k":100,
1938 | "ix":4
1939 | },
1940 | "w":{
1941 | "a":1,
1942 | "k":[
1943 | {
1944 | "i":{
1945 | "x":[
1946 | 0.833
1947 | ],
1948 | "y":[
1949 | 0.833
1950 | ]
1951 | },
1952 | "o":{
1953 | "x":[
1954 | 0.167
1955 | ],
1956 | "y":[
1957 | 0.167
1958 | ]
1959 | },
1960 | "t":-40,
1961 | "s":[
1962 | 6
1963 | ]
1964 | },
1965 | {
1966 | "i":{
1967 | "x":[
1968 | 0.833
1969 | ],
1970 | "y":[
1971 | 0.833
1972 | ]
1973 | },
1974 | "o":{
1975 | "x":[
1976 | 0.167
1977 | ],
1978 | "y":[
1979 | 0.167
1980 | ]
1981 | },
1982 | "t":-20,
1983 | "s":[
1984 | 5
1985 | ]
1986 | },
1987 | {
1988 | "i":{
1989 | "x":[
1990 | 0.833
1991 | ],
1992 | "y":[
1993 | 0.833
1994 | ]
1995 | },
1996 | "o":{
1997 | "x":[
1998 | 0.167
1999 | ],
2000 | "y":[
2001 | 0.167
2002 | ]
2003 | },
2004 | "t":0,
2005 | "s":[
2006 | 4
2007 | ]
2008 | },
2009 | {
2010 | "t":20,
2011 | "s":[
2012 | 3
2013 | ]
2014 | }
2015 | ],
2016 | "ix":5
2017 | },
2018 | "lc":1,
2019 | "lj":1,
2020 | "ml":4,
2021 | "bm":0,
2022 | "nm":"描边 1",
2023 | "mn":"ADBE Vector Graphic - Stroke",
2024 | "hd":false
2025 | },
2026 | {
2027 | "ty":"tr",
2028 | "p":{
2029 | "a":0,
2030 | "k":[
2031 | 57.428,
2032 | 217.928
2033 | ],
2034 | "ix":2
2035 | },
2036 | "a":{
2037 | "a":0,
2038 | "k":[
2039 | 0,
2040 | 0
2041 | ],
2042 | "ix":1
2043 | },
2044 | "s":{
2045 | "a":0,
2046 | "k":[
2047 | 100,
2048 | 100
2049 | ],
2050 | "ix":3
2051 | },
2052 | "r":{
2053 | "a":0,
2054 | "k":0,
2055 | "ix":6
2056 | },
2057 | "o":{
2058 | "a":0,
2059 | "k":100,
2060 | "ix":7
2061 | },
2062 | "sk":{
2063 | "a":0,
2064 | "k":0,
2065 | "ix":4
2066 | },
2067 | "sa":{
2068 | "a":0,
2069 | "k":0,
2070 | "ix":5
2071 | },
2072 | "nm":"变换"
2073 | }
2074 | ],
2075 | "nm":"椭圆 1",
2076 | "np":3,
2077 | "cix":2,
2078 | "bm":0,
2079 | "ix":1,
2080 | "mn":"ADBE Vector Group",
2081 | "hd":false
2082 | }
2083 | ],
2084 | "ip":-40,
2085 | "op":1486,
2086 | "st":-40,
2087 | "bm":0
2088 | }
2089 | ],
2090 | "markers":[
2091 |
2092 | ]
2093 | }
2094 |
--------------------------------------------------------------------------------
/assets/splash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JaeWangL/blabla-mobile/c25785b8472241c91896c39eced27f6fdbade9db/assets/splash.png
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = function(api) {
2 | api.cache(true);
3 | return {
4 | presets: ['babel-preset-expo'],
5 | plugins: [
6 | [
7 | "module-resolver",
8 | {
9 | extensions: [".ts", ".tsx", ".jsx", ".js", ".json", ".svg", ".jpg"],
10 | alias: {
11 | "@": ["./src"],
12 | "@assets": "./assets",
13 | },
14 | },
15 | ],
16 | 'react-native-reanimated/plugin',
17 | ],
18 | };
19 | };
20 |
--------------------------------------------------------------------------------
/metro.config.js:
--------------------------------------------------------------------------------
1 | const { getDefaultConfig } = require("expo/metro-config");
2 |
3 | module.exports = (() => {
4 | const config = getDefaultConfig(__dirname);
5 |
6 | const { transformer, resolver } = config;
7 |
8 | config.transformer = {
9 | ...transformer,
10 | babelTransformerPath: require.resolve("react-native-svg-transformer"),
11 | };
12 | config.resolver = {
13 | ...resolver,
14 | assetExts: resolver.assetExts.filter((ext) => ext !== "svg"),
15 | sourceExts: [...resolver.sourceExts, "svg"],
16 | };
17 |
18 | return config;
19 | })();
20 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "blablamobile",
3 | "version": "1.0.0",
4 | "main": "node_modules/expo/AppEntry.js",
5 | "scripts": {
6 | "start": "expo start",
7 | "android": "expo start --android",
8 | "ios": "expo start --ios",
9 | "web": "expo start --web",
10 | "eject": "expo eject",
11 | "lint": "eslint --cache --ext .ts,.tsx,.js ./",
12 | "lint:fix": "eslint --fix --config .eslintrc.commit.js --cache --ext .ts,.tsx,.js ./",
13 | "prettify": "prettier --write ./ --ignore-path ../../.prettierignore",
14 | "pre-commit": "lint-staged"
15 | },
16 | "dependencies": {
17 | "@expo/vector-icons": "^12.0.5",
18 | "@react-native-async-storage/async-storage": "~1.15.0",
19 | "@react-native-masked-view/masked-view": "0.2.6",
20 | "@react-navigation/bottom-tabs": "^6.2.0",
21 | "@react-navigation/native": "^6.0.8",
22 | "@react-navigation/stack": "^6.1.1",
23 | "@stomp/stompjs": "^6.1.2",
24 | "axios": "^0.26.0",
25 | "babel-plugin-module-resolver": "^4.1.0",
26 | "dayjs": "^1.10.8",
27 | "expo": "~44.0.0",
28 | "expo-app-loading": "~1.3.0",
29 | "expo-application": "~4.0.1",
30 | "expo-asset": "~8.4.6",
31 | "expo-constants": "~13.0.1",
32 | "expo-font": "~10.0.4",
33 | "expo-image-picker": "~12.0.1",
34 | "expo-localization": "~12.0.0",
35 | "expo-location": "~14.0.1",
36 | "expo-splash-screen": "~0.14.1",
37 | "expo-status-bar": "~1.2.0",
38 | "expo-task-manager": "~10.1.0",
39 | "i18n-js": "^3.8.0",
40 | "lottie-react-native": "5.0.1",
41 | "react": "17.0.1",
42 | "react-dom": "17.0.1",
43 | "react-fast-compare": "^3.2.0",
44 | "react-native": "0.64.3",
45 | "react-native-appearance": "~0.3.3",
46 | "react-native-gesture-handler": "~2.1.0",
47 | "react-native-maps": "0.29.4",
48 | "react-native-reanimated": "~2.3.1",
49 | "react-native-safe-area-context": "3.3.2",
50 | "react-native-screens": "~3.10.1",
51 | "react-native-svg": "12.1.1",
52 | "react-native-toast-message": "^2.1.1",
53 | "react-native-ui-lib": "^6.10.1",
54 | "react-native-web": "0.17.1",
55 | "react-query": "^3.34.16",
56 | "recoil": "^0.6.1",
57 | "socket.io-client": "^4.4.1",
58 | "sockjs-client": "^1.6.0",
59 | "text-encoding": "^0.7.0"
60 | },
61 | "devDependencies": {
62 | "@babel/core": "^7.12.9",
63 | "@types/i18n-js": "^3.8.2",
64 | "@types/react": "~17.0.21",
65 | "@types/react-native": "~0.64.12",
66 | "@types/sockjs-client": "^1.5.1",
67 | "@typescript-eslint/eslint-plugin": "^5.12.1",
68 | "@typescript-eslint/parser": "^5.12.1",
69 | "eslint": "^8.9.0",
70 | "eslint-config-airbnb": "^19.0.2",
71 | "eslint-config-airbnb-typescript": "^16.1.0",
72 | "eslint-config-prettier": "^8.4.0",
73 | "eslint-import-resolver-typescript": "^2.5.0",
74 | "eslint-plugin-import": "^2.25.4",
75 | "eslint-plugin-jsx-a11y": "^6.5.1",
76 | "eslint-plugin-prettier": "^4.0.0",
77 | "eslint-plugin-react": "^7.28.0",
78 | "eslint-plugin-react-hooks": "^4.3.0",
79 | "husky": "^7.0.4",
80 | "jest": "^26.3.0",
81 | "lint-staged": "^12.3.4",
82 | "prettier": "^2.5.1",
83 | "react-native-svg-transformer": "^1.0.0",
84 | "typescript": "~4.5.5"
85 | },
86 | "private": true
87 | }
88 |
--------------------------------------------------------------------------------
/src/components/customAppbar/index.tsx:
--------------------------------------------------------------------------------
1 | import { memo, useCallback, useMemo } from 'react';
2 | import IsEqual from 'react-fast-compare';
3 | import { Text, TouchableOpacity, View, ViewProps } from 'react-native-ui-lib';
4 | import { useNavigation } from '@react-navigation/native';
5 | import IcArrowLeft from '@assets/icons/ic_arrow_left.svg';
6 | import IcArrowLeftWhite from '@assets/icons/ic_arrow_left_white.svg';
7 | import Divider from '../divider';
8 | import { styles } from './styles';
9 |
10 | type AlignmentProp = 'start' | 'center';
11 |
12 | const getBackgroundColor = (transparent?: boolean) => {
13 | if (transparent) {
14 | return { backgroundColor: 'transparent' };
15 | }
16 | return { backgroundColor: 'white' };
17 | };
18 |
19 | const getAlignmentDependentStyles = (alignment: AlignmentProp) => {
20 | if (alignment === 'center') {
21 | return {
22 | container: styles.containerCentered,
23 | titleContainer: styles.titleContainerCentered,
24 | };
25 | }
26 |
27 | return {
28 | rightControlsContainer: styles.rightControlsContainerStart,
29 | };
30 | };
31 |
32 | type CustomAppBarProps = {
33 | title: string;
34 | subtitle?: string;
35 | accessoryLeft?: JSX.Element;
36 | accessoryRight?: JSX.Element;
37 | alignment?: AlignmentProp;
38 | transparent?: boolean;
39 | goBack?: boolean;
40 | } & ViewProps;
41 |
42 | function CustomAppBar(props: CustomAppBarProps): JSX.Element {
43 | const { accessoryLeft, accessoryRight, alignment, goBack, subtitle, title, transparent, style } = props;
44 | const navigation = useNavigation();
45 | const alignmentStyles = useMemo(() => getAlignmentDependentStyles(alignment!), []);
46 | const backgroundStyles = useMemo(() => getBackgroundColor(transparent!), []);
47 |
48 | const onBackButtonPress = useCallback((): void => {
49 | if (goBack && navigation.canGoBack()) {
50 | navigation.goBack();
51 | }
52 | }, []);
53 |
54 | const renderLeftControl = useCallback((): JSX.Element | null => {
55 | if (goBack) {
56 | return (
57 |
58 | {transparent ? : }
59 |
60 | );
61 | }
62 | if (accessoryLeft) {
63 | return accessoryLeft;
64 | }
65 |
66 | return null;
67 | }, []);
68 |
69 | return (
70 | <>
71 |
72 | {renderLeftControl()}
73 |
74 | {title}
75 | {subtitle && {subtitle}}
76 |
77 |
78 | {accessoryRight && accessoryRight}
79 |
80 |
81 | {!transparent && }
82 | >
83 | );
84 | }
85 |
86 | CustomAppBar.defaultProps = {
87 | subtitle: undefined,
88 | accessoryLeft: undefined,
89 | accessoryRight: undefined,
90 | alignment: 'start',
91 | transparent: false,
92 | goBack: false,
93 | } as CustomAppBarProps;
94 |
95 | export default memo(CustomAppBar, IsEqual);
96 |
--------------------------------------------------------------------------------
/src/components/customAppbar/styles.ts:
--------------------------------------------------------------------------------
1 | import { StyleSheet } from 'react-native';
2 | import { zIndices, APPBAR_HEIGHT } from '@/themes';
3 |
4 | export const styles = StyleSheet.create({
5 | container: {
6 | flexDirection: 'row',
7 | alignItems: 'center',
8 | height: APPBAR_HEIGHT,
9 | zIndex: zIndices.appbar,
10 | },
11 | containerCentered: {
12 | justifyContent: 'space-between',
13 | },
14 | backButton: {
15 | paddingRight: 12,
16 | },
17 | title: {
18 | fontSize: 20,
19 | fontFamily: 'PretendardBold',
20 | },
21 | subtitle: {
22 | fontSize: 14,
23 | fontFamily: 'PretendardRegular',
24 | },
25 | titleContainerCentered: {
26 | ...StyleSheet.absoluteFillObject,
27 | justifyContent: 'center',
28 | alignItems: 'center',
29 | },
30 | titleContainer: {
31 | flexDirection: 'row',
32 | flex: 1,
33 | },
34 | leftControlContainer: {
35 | flexDirection: 'row',
36 | zIndex: 1,
37 | marginLeft: 12,
38 | },
39 | rightControlsContainer: {
40 | flexDirection: 'row',
41 | zIndex: 1,
42 | marginRight: 12,
43 | },
44 | rightControlsContainerStart: {
45 | flex: 0,
46 | justifyContent: 'flex-end',
47 | },
48 | });
49 |
--------------------------------------------------------------------------------
/src/components/customHeader/index.tsx:
--------------------------------------------------------------------------------
1 | import { memo } from 'react';
2 | import IsEqual from 'react-fast-compare';
3 | import { Text } from 'react-native-ui-lib';
4 | import { styles } from './styles';
5 |
6 | type CustomHeaderProps = {
7 | title: string;
8 | };
9 | function CustomHeader(props: CustomHeaderProps): JSX.Element {
10 | const { title } = props;
11 |
12 | return {title};
13 | }
14 |
15 | export default memo(CustomHeader, IsEqual);
16 |
--------------------------------------------------------------------------------
/src/components/customHeader/styles.ts:
--------------------------------------------------------------------------------
1 | import { StyleSheet } from 'react-native';
2 |
3 | export const styles = StyleSheet.create({
4 | title: {
5 | fontSize: 26,
6 | fontFamily: 'PretendardBold',
7 | marginTop: 24,
8 | marginLeft: 24,
9 | marginBottom: 12,
10 | },
11 | });
12 |
--------------------------------------------------------------------------------
/src/components/divider/index.tsx:
--------------------------------------------------------------------------------
1 | import { memo } from 'react';
2 | import IsEqual from 'react-fast-compare';
3 | import { View } from 'react-native-ui-lib';
4 |
5 | type DividerProps = {
6 | color?: string;
7 | height?: number;
8 | margin?: number;
9 | };
10 |
11 | function Divider(props: DividerProps): JSX.Element {
12 | const { color, height, margin } = props;
13 |
14 | return ;
15 | }
16 |
17 | Divider.defaultProps = {
18 | color: '#DDDDDD',
19 | height: 1,
20 | margin: 0,
21 | } as DividerProps;
22 |
23 | export default memo(Divider, IsEqual);
24 |
--------------------------------------------------------------------------------
/src/components/emptyScreen/index.tsx:
--------------------------------------------------------------------------------
1 | import { memo } from 'react';
2 | import IsEqual from 'react-fast-compare';
3 | import { AnimatedImage, Text, View } from 'react-native-ui-lib';
4 | import ImgEmpty from '@assets/images/img_empty.png';
5 | import { translate } from '@/i18n';
6 | import { styles } from './styles';
7 |
8 | function EmptyScreen(): JSX.Element {
9 | return (
10 |
11 |
12 | {translate('common.noData')}
13 |
14 | );
15 | }
16 |
17 | export default memo(EmptyScreen, IsEqual);
18 |
--------------------------------------------------------------------------------
/src/components/emptyScreen/styles.ts:
--------------------------------------------------------------------------------
1 | import { StyleSheet } from 'react-native';
2 | import { defaultTheme } from '@/themes';
3 |
4 | export const styles = StyleSheet.create({
5 | wrapper: {
6 | flex: 1,
7 | justifyContent: 'center',
8 | alignItems: 'center',
9 | },
10 | icon: {
11 | width: 128,
12 | height: 128,
13 | },
14 | text: {
15 | paddingTop: 12,
16 | fontSize: 20,
17 | color: defaultTheme.captionDefault,
18 | fontFamily: 'PretendardBold',
19 | },
20 | });
21 |
--------------------------------------------------------------------------------
/src/components/floatingButton/index.tsx:
--------------------------------------------------------------------------------
1 | import { memo } from 'react';
2 | import IsEqual from 'react-fast-compare';
3 | import { TouchableOpacity } from 'react-native';
4 | import Animated from 'react-native-reanimated';
5 | import { defaultTheme } from '@/themes';
6 | import { styles } from './styles';
7 |
8 | type FloatingButtonProps = {
9 | onPress: () => void;
10 | size?: number;
11 | backgroundColor?: string;
12 | offset?: number;
13 | };
14 |
15 | function FloatingButton(props: FloatingButtonProps): JSX.Element {
16 | const { backgroundColor, offset, size, onPress } = props;
17 |
18 | return (
19 |
30 |
41 | +
42 |
43 |
44 | );
45 | }
46 |
47 | FloatingButton.defaultProps = {
48 | size: 60,
49 | backgroundColor: defaultTheme.primary,
50 | offset: 24,
51 | } as FloatingButtonProps;
52 |
53 | export default memo(FloatingButton, IsEqual);
54 |
--------------------------------------------------------------------------------
/src/components/floatingButton/styles.ts:
--------------------------------------------------------------------------------
1 | import { StyleSheet } from 'react-native';
2 |
3 | export const styles = StyleSheet.create({
4 | container: {
5 | position: 'absolute',
6 | },
7 | fabButton: {
8 | alignItems: 'center',
9 | justifyContent: 'center',
10 | },
11 | text: {
12 | fontSize: 36,
13 | color: '#EFFBFA',
14 | },
15 | });
16 |
--------------------------------------------------------------------------------
/src/components/keyboardAwareScrollView/index.tsx:
--------------------------------------------------------------------------------
1 | import { memo, useEffect, useState } from 'react';
2 | import IsEqual from 'react-fast-compare';
3 | import { Dimensions, KeyboardAvoidingView, NativeModules, Platform, ScrollView } from 'react-native';
4 | import { styles } from './styles';
5 |
6 | type KeyboardAwareScrollViewProps = {
7 | children: JSX.Element;
8 | useScroll?: boolean;
9 | };
10 |
11 | const { StatusBarManager } = NativeModules;
12 |
13 | function KeyboardAwareScrollView(props: KeyboardAwareScrollViewProps): JSX.Element {
14 | const { children, useScroll } = props;
15 | const [verticalOffset, setVerticalOffset] = useState(0);
16 |
17 | useEffect(() => {
18 | if (Platform.OS === 'ios') {
19 | // @ts-ignore
20 | StatusBarManager.getHeight((statusBarFrameData) => {
21 | setVerticalOffset(statusBarFrameData.height);
22 | });
23 | } else {
24 | setVerticalOffset(-Dimensions.get('window').height);
25 | }
26 | }, []);
27 |
28 | return (
29 |
30 | {useScroll ? {children} : children}
31 |
32 | );
33 | }
34 |
35 | KeyboardAwareScrollView.defaultProps = {
36 | useScroll: false,
37 | } as KeyboardAwareScrollViewProps;
38 |
39 | export default memo(KeyboardAwareScrollView, IsEqual);
40 |
--------------------------------------------------------------------------------
/src/components/keyboardAwareScrollView/styles.ts:
--------------------------------------------------------------------------------
1 | import { StyleSheet } from 'react-native';
2 |
3 | export const styles = StyleSheet.create({
4 | wrapper: {
5 | flex: 1,
6 | backgroundColor: 'white',
7 | },
8 | });
9 |
--------------------------------------------------------------------------------
/src/configs/api_keys.ts:
--------------------------------------------------------------------------------
1 | export const apiKeys = {
2 | chatDomain: 'https://blablachat.azurewebsites.net/',
3 | locationsDomain: 'https://blablalocations.azurewebsites.net/',
4 | locationsStomp: 'wss://blablalocations.azurewebsites.net/locations-stomp',
5 | locationsSockJS: 'https://blablalocations.azurewebsites.net/locations-stomp',
6 | postsDomain: 'https://blablaposts.azurewebsites.net/posts/',
7 | uploadDomain: 'https://blablaupload.azurewebsites.net/',
8 | };
9 |
10 | export const queryKeys = {
11 | postsByDistance: 'postsByDistance',
12 | };
13 |
--------------------------------------------------------------------------------
/src/configs/recoil_keys.ts:
--------------------------------------------------------------------------------
1 | export enum RecoilStorageKeys {
2 | SETTINGS = 'recoil_settings',
3 | }
4 |
--------------------------------------------------------------------------------
/src/configs/screen_types.ts:
--------------------------------------------------------------------------------
1 | import { PostPreviewDTO } from '../dtos/post_dtos';
2 |
3 | export enum ScreenTypes {
4 | // UnPermissioned
5 | STACK_UN_PERMISSIONED = 'StackUnPermissioned',
6 | ON_BOARDING = 'Onboarding',
7 |
8 | // Permissioned
9 | STACK_PERMISSIONED = 'StackPermissioned',
10 |
11 | STACK_ARCHIVES = 'StackArchives',
12 | ARCHIVES = 'Archives',
13 |
14 | STACK_HOME = 'StackHome',
15 | HOME = 'Home',
16 |
17 | STACK_SETTINGS = 'StackSettings',
18 | SETTINGS = 'Settings',
19 | INFO_DEVELOPERS = 'InfoDevelopers',
20 |
21 | // Shared by tabs
22 | SHARED_POST_DETAIL = 'SharedPostDetail',
23 | SHARED_POST_WRITE = 'SharedPostWrite',
24 | SHARED_POST_CHAT = 'SharedPostChat',
25 | }
26 |
27 | export type UnPermissionedParamsList = {
28 | [ScreenTypes.ON_BOARDING]: undefined;
29 | };
30 |
31 | export type PermissionedParamsList = {
32 | [ScreenTypes.STACK_ARCHIVES]: undefined;
33 | [ScreenTypes.STACK_HOME]: undefined;
34 | [ScreenTypes.STACK_SETTINGS]: undefined;
35 | };
36 |
37 | export type ArchivesParamsList = {
38 | [ScreenTypes.ARCHIVES]: undefined;
39 | [ScreenTypes.SHARED_POST_DETAIL]: { post: PostPreviewDTO };
40 | [ScreenTypes.SHARED_POST_WRITE]: undefined;
41 | [ScreenTypes.SHARED_POST_CHAT]: { post: PostPreviewDTO };
42 | };
43 |
44 | export type HomeParamsList = {
45 | [ScreenTypes.HOME]: undefined;
46 | [ScreenTypes.SHARED_POST_DETAIL]: { post: PostPreviewDTO };
47 | [ScreenTypes.SHARED_POST_WRITE]: undefined;
48 | [ScreenTypes.SHARED_POST_CHAT]: { post: PostPreviewDTO };
49 | };
50 |
51 | export type SettingsParamsList = {
52 | [ScreenTypes.SETTINGS]: undefined;
53 | [ScreenTypes.INFO_DEVELOPERS]: undefined;
54 | };
55 |
56 | export type RootStackParamList = {
57 | [ScreenTypes.STACK_UN_PERMISSIONED]: undefined;
58 | [ScreenTypes.STACK_PERMISSIONED]: undefined;
59 | } & UnPermissionedParamsList &
60 | PermissionedParamsList;
61 |
--------------------------------------------------------------------------------
/src/configs/socket_keys.ts:
--------------------------------------------------------------------------------
1 | export enum ChatPubDestination {
2 | JOIN_ROOM = 'joinRoom',
3 | SEND_MESSAGE = 'sendMessage',
4 | }
5 |
6 | export enum ChatSubDestination {
7 | RATE_LIMITED = 'rateLimited',
8 | GET_PROFILE = 'getProfile',
9 | JOINED_NEW_MEMBER = 'joinedNewMember',
10 | LEAVED_EXISTING_MEMBER = 'leavedExistingMember',
11 | NEW_MESSAGE = 'newMessage',
12 | }
13 |
14 | export enum LocationSocketDestination {
15 | // Send
16 | UPDATE_LOCATION = '/app/location/update',
17 |
18 | // Receive Notifications
19 | CREATED_NEW_POST = '/user/queue/post/new',
20 | }
21 |
--------------------------------------------------------------------------------
/src/configs/tasks_types.ts:
--------------------------------------------------------------------------------
1 | export enum TaskTypes {
2 | BACKGROUND_LOCATION = 'background-location-task',
3 | }
4 |
--------------------------------------------------------------------------------
/src/dtos/chat_dtos.ts:
--------------------------------------------------------------------------------
1 | export interface JoinRoomRequest {
2 | roomId: string;
3 |
4 | /**
5 | * 1: Android
6 | * 2: iOS
7 | */
8 | deviceType: 1 | 2;
9 | deviceId: string;
10 | }
11 |
12 | export interface SendMessageRequest {
13 | roomId: string;
14 | nickName: string;
15 | message: string;
16 | }
17 |
18 | export interface SentMessage {
19 | nickName: string;
20 | message: string;
21 | createdAt: Date;
22 | }
23 |
24 | export interface JoinedNewMember {
25 | nickName: string;
26 | joinedAt: Date;
27 | }
28 |
29 | export interface LeavedExistingMember {
30 | nickName: string;
31 | leavedAt: Date;
32 | }
33 |
--------------------------------------------------------------------------------
/src/dtos/location_dtos.ts:
--------------------------------------------------------------------------------
1 | export interface UpdateLocationRequest {
2 | deviceType: 1 | 2;
3 | deviceId: string;
4 | latitude: number;
5 | longitude: number;
6 | }
7 |
8 | export interface NewPostCreatedMessage {
9 | title: string;
10 | }
11 |
--------------------------------------------------------------------------------
/src/dtos/post_dtos.ts:
--------------------------------------------------------------------------------
1 | export interface CreatePostRequest {
2 | deviceType: 1 | 2;
3 | deviceId: string;
4 | latitude: number;
5 | longitude: number;
6 | country?: string;
7 | state?: string;
8 | city?: string;
9 | street?: string;
10 | detailAddress?: string;
11 | zipCode?: string;
12 | title: string;
13 | contents: string;
14 | thumbnailUrl?: string;
15 | originalFileName?: string;
16 | }
17 |
18 | export interface PostCreatedDTO {
19 | id: string;
20 | deviceType: 1 | 2;
21 | deviceId: string;
22 | }
23 |
24 | export interface PostDetailDTO {
25 | id: string;
26 | latitude: number;
27 | longitude: number;
28 | title: string;
29 | contents: string;
30 | thumbnailUrl?: string;
31 | joinedUsers: number;
32 | createdAt: Date;
33 | updatedAt: Date;
34 | }
35 |
36 | export interface PostPreviewDTO {
37 | id: string;
38 | latitude: number;
39 | longitude: number;
40 | title: string;
41 | contentsSnippet: string;
42 | thumbnailUrl?: string;
43 | distanceM: number;
44 | joinedUsers: number;
45 | createdAt: Date;
46 | updatedAt: Date;
47 | }
48 |
--------------------------------------------------------------------------------
/src/dtos/socket_dtos.ts:
--------------------------------------------------------------------------------
1 | export interface RateLimited {
2 | retryRemainingMs: number;
3 | }
4 |
--------------------------------------------------------------------------------
/src/dtos/upload_dtos.ts:
--------------------------------------------------------------------------------
1 | export interface UploadedThumbnailDTO {
2 | thumbnailUrl: string;
3 | origianlFileName: string;
4 | }
5 |
--------------------------------------------------------------------------------
/src/helpers/device_utils.ts:
--------------------------------------------------------------------------------
1 | import * as Application from 'expo-application';
2 | import { Platform } from 'react-native';
3 |
4 | export interface DeviceInfo {
5 | /**
6 | * 1: Android
7 | * 2: iOS
8 | */
9 | deviceType: 1 | 2;
10 | deviceId: string;
11 | }
12 |
13 | export async function getDeviceInfo(): Promise {
14 | let deviceId: string | null;
15 | if (Platform.OS === 'android') {
16 | deviceId = Application.androidId;
17 | } else {
18 | deviceId = await Application.getIosIdForVendorAsync();
19 | }
20 | if (!deviceId) {
21 | return undefined;
22 | }
23 |
24 | return {
25 | deviceType: Platform.OS === 'android' ? 1 : 2,
26 | deviceId,
27 | };
28 | }
29 |
--------------------------------------------------------------------------------
/src/hooks/use_chat_socket.ts:
--------------------------------------------------------------------------------
1 | import { useCallback, useEffect, useRef, useState } from 'react';
2 | import { Alert } from 'react-native';
3 | import io, { Socket } from 'socket.io-client';
4 | import { apiKeys } from '../configs/api_keys';
5 | import { ChatPubDestination, ChatSubDestination } from '../configs/socket_keys';
6 | import { JoinRoomRequest, JoinedNewMember, LeavedExistingMember, SentMessage } from '../dtos/chat_dtos';
7 | import { RateLimited } from '../dtos/socket_dtos';
8 | import { getDeviceInfo } from '../helpers/device_utils';
9 | import { translate } from '../i18n';
10 |
11 | type ChatSocketProps = {
12 | roomId: string;
13 | subNewMemberJoined: (res: JoinedNewMember) => void;
14 | subMemberLeaved: (res: LeavedExistingMember) => void;
15 | subNewMessage: (res: SentMessage) => void;
16 | handleDisconnect: () => void;
17 | };
18 |
19 | type ChatSocketType = {
20 | chatSocket: Socket | null;
21 | nickName: string;
22 | };
23 |
24 | export function useChatSocket(props: ChatSocketProps): ChatSocketType {
25 | const { handleDisconnect, roomId, subMemberLeaved, subNewMemberJoined, subNewMessage } = props;
26 | const socket = useRef(null);
27 | const [nickName, setNickName] = useState('');
28 |
29 | const initAsync = useCallback(async (): Promise => {
30 | if (socket.current) {
31 | return;
32 | }
33 |
34 | const chatSocket = io(apiKeys.chatDomain, {
35 | reconnectionAttempts: 2,
36 | transports: ['websocket'],
37 | path: '/ws-chat/',
38 | upgrade: false,
39 | });
40 | const deviceInfo = await getDeviceInfo();
41 | if (!deviceInfo) {
42 | return;
43 | }
44 |
45 | chatSocket.emit(ChatPubDestination.JOIN_ROOM, {
46 | roomId,
47 | deviceType: deviceInfo.deviceType,
48 | deviceId: deviceInfo.deviceId,
49 | } as JoinRoomRequest);
50 |
51 | chatSocket.on(ChatSubDestination.GET_PROFILE, setNickName);
52 | chatSocket.on(ChatSubDestination.JOINED_NEW_MEMBER, subNewMemberJoined);
53 | chatSocket.on(ChatSubDestination.LEAVED_EXISTING_MEMBER, subMemberLeaved);
54 | chatSocket.on(ChatSubDestination.NEW_MESSAGE, subNewMessage);
55 | chatSocket.on(ChatSubDestination.RATE_LIMITED, (res: RateLimited) => {
56 | Alert.alert(
57 | translate('dialogs.rateLimitedTitle'),
58 | translate('dialogs.rateLimitedDesc', { retryRemainingS: res.retryRemainingMs / 1000 }),
59 | [{ text: translate('common.ok') }],
60 | );
61 | });
62 |
63 | socket.current = chatSocket;
64 | }, []);
65 |
66 | const leave = useCallback((): void => {
67 | if (handleDisconnect) {
68 | handleDisconnect();
69 | }
70 | socket.current?.disconnect();
71 | setNickName('');
72 | socket.current = null;
73 | }, [socket.current]);
74 |
75 | useEffect(() => {
76 | initAsync();
77 |
78 | return () => {
79 | leave();
80 | };
81 | }, []);
82 |
83 | return { chatSocket: socket.current, nickName };
84 | }
85 |
--------------------------------------------------------------------------------
/src/hooks/use_location_socket.ts:
--------------------------------------------------------------------------------
1 | import * as Location from 'expo-location';
2 | import { useCallback, useEffect, useRef } from 'react';
3 | import { AppState } from 'react-native';
4 | import Toast from 'react-native-toast-message';
5 | import { useSetRecoilState } from 'recoil';
6 | import SockJS from 'sockjs-client';
7 | import { Client } from '@stomp/stompjs';
8 | import { apiKeys } from '@/configs/api_keys';
9 | import { LocationSocketDestination } from '@/configs/socket_keys';
10 | import { getDeviceInfo } from '@/helpers/device_utils';
11 | import { translate } from '@/i18n';
12 | import { locationAtom } from '@/recoils/location_states';
13 |
14 | export function useLocationSocket(): Client | null {
15 | const setLocation = useSetRecoilState(locationAtom);
16 | const socketRef = useRef(null);
17 | const watchRef = useRef(null);
18 |
19 | const onLocationChnaged = useCallback(async (socket: Client, loc: Location.LocationObject): Promise => {
20 | const deviceInfo = await getDeviceInfo();
21 | if (!deviceInfo) {
22 | return;
23 | }
24 |
25 | setLocation({ latitude: loc.coords.latitude, longitude: loc.coords.longitude });
26 |
27 | if (!socket.connected) {
28 | return;
29 | }
30 | socket.publish({
31 | destination: LocationSocketDestination.UPDATE_LOCATION,
32 | body: JSON.stringify({
33 | deviceType: deviceInfo.deviceType,
34 | deviceId: deviceInfo.deviceId,
35 | latitude: loc.coords.latitude,
36 | longitude: loc.coords.longitude,
37 | }),
38 | });
39 | }, []);
40 |
41 | const connectAsync = useCallback(async (): Promise => {
42 | if (socketRef.current) {
43 | return;
44 | }
45 |
46 | const locationSocket = new Client();
47 | locationSocket.configure({
48 | brokerURL: apiKeys.locationsStomp,
49 | reconnectDelay: 5000,
50 | heartbeatIncoming: 4000,
51 | heartbeatOutgoing: 4000,
52 | webSocketFactory() {
53 | return new SockJS(apiKeys.locationsSockJS);
54 | },
55 | debug(str) {
56 | // TODO
57 | },
58 | onConnect(frame) {
59 | locationSocket.subscribe(LocationSocketDestination.CREATED_NEW_POST, ({ body }: { body: string }) => {
60 | Toast.show({
61 | type: 'success',
62 | text1: translate('dialogs.newPostTitle'),
63 | text2: body,
64 | });
65 | });
66 | },
67 | onStompError(frame) {
68 | // TODO
69 | },
70 | });
71 | locationSocket.activate();
72 | socketRef.current = locationSocket;
73 |
74 | const lastPosition = await Location.getLastKnownPositionAsync();
75 | if (lastPosition) {
76 | onLocationChnaged(locationSocket, lastPosition);
77 | }
78 | }, []);
79 |
80 | const leave = useCallback((): void => {
81 | watchRef.current?.remove();
82 | socketRef.current?.deactivate();
83 | }, [watchRef.current, socketRef.current]);
84 |
85 | const onAppStateChange = useCallback(
86 | (nextAppState: 'active' | 'background' | 'inactive' | 'unknown' | 'extension'): void => {
87 | if (nextAppState !== 'active') {
88 | leave();
89 | } else {
90 | connectAsync();
91 | }
92 | },
93 | [socketRef.current],
94 | );
95 |
96 | useEffect(() => {
97 | connectAsync();
98 |
99 | AppState.addEventListener('change', onAppStateChange);
100 |
101 | return () => {
102 | AppState.removeEventListener('change', onAppStateChange);
103 | leave();
104 | };
105 | }, []);
106 |
107 | useEffect(() => {
108 | const initAsync = async (): Promise => {
109 | if (!socketRef.current) {
110 | return;
111 | }
112 |
113 | watchRef.current = await Location.watchPositionAsync(
114 | {
115 | accuracy: Location.Accuracy.Lowest,
116 | timeInterval: 2000,
117 | distanceInterval: 5,
118 | },
119 | (loc) => {
120 | onLocationChnaged(socketRef.current!, loc);
121 | },
122 | );
123 | };
124 |
125 | initAsync();
126 | }, [socketRef.current, socketRef.current?.connected]);
127 |
128 | return socketRef.current;
129 | }
130 |
--------------------------------------------------------------------------------
/src/i18n/i18n.ts:
--------------------------------------------------------------------------------
1 | import * as Localization from 'expo-localization';
2 | import i18n from 'i18n-js';
3 | import en from '@assets/langs/en.json';
4 | import ko from '@assets/langs/ko.json';
5 |
6 | i18n.fallbacks = true;
7 | i18n.translations = { en, ko };
8 | i18n.locale = Localization.locale || 'ko';
9 |
10 | /**
11 | * Builds up valid keypaths for translations.
12 | * Update to your default locale of choice if not English.
13 | */
14 | type DefaultLocale = typeof en;
15 | export type TxKeyPath = RecursiveKeyOf;
16 |
17 | type RecursiveKeyOf> = {
18 | [TKey in keyof TObj & string]: TObj[TKey] extends Record
19 | ? // @ts-ignore
20 | TKey | `${TKey}.${RecursiveKeyOf}`
21 | : TKey;
22 | }[keyof TObj & string];
23 |
--------------------------------------------------------------------------------
/src/i18n/index.ts:
--------------------------------------------------------------------------------
1 | import './i18n';
2 |
3 | export * from './i18n';
4 | export * from './translate';
5 |
--------------------------------------------------------------------------------
/src/i18n/translate.ts:
--------------------------------------------------------------------------------
1 | import i18n from 'i18n-js';
2 | import { TxKeyPath } from './i18n';
3 |
4 | /**
5 | * Translates text.
6 | *
7 | * @param key The i18n key.
8 | */
9 | export function translate(key: TxKeyPath, options?: i18n.TranslateOptions): string {
10 | return key ? i18n.t(key, options) : '';
11 | }
12 |
--------------------------------------------------------------------------------
/src/main.tsx:
--------------------------------------------------------------------------------
1 | import AppLoading from 'expo-app-loading';
2 | import * as Localization from 'expo-localization';
3 | import { useAssets } from 'expo-asset';
4 | import { useFonts } from 'expo-font';
5 | import * as SplashScreen from 'expo-splash-screen';
6 | import { StatusBar } from 'expo-status-bar';
7 | import i18n from 'i18n-js';
8 | import { useEffect, Suspense } from 'react';
9 | import { AppearanceProvider } from 'react-native-appearance';
10 | import { SafeAreaProvider } from 'react-native-safe-area-context';
11 | import { QueryClient, QueryClientProvider } from 'react-query';
12 | import { RecoilRoot } from 'recoil';
13 | import { RootNavigator } from './navigation/root_navigator';
14 | import './i18n';
15 |
16 | const queryClient = new QueryClient();
17 |
18 | SplashScreen.preventAutoHideAsync();
19 |
20 | function App(): JSX.Element {
21 | const [fontsLoaded] = useFonts({
22 | /* eslint-disable global-require */
23 | PretendardBold: require('../assets/fonts/Pretendard-Bold.ttf'),
24 | PretendardExtraBold: require('../assets/fonts/Pretendard-ExtraBold.ttf'),
25 | PretendardRegular: require('../assets/fonts/Pretendard-Regular.ttf'),
26 | /* eslint-enable */
27 | });
28 |
29 | useEffect(() => {
30 | if (fontsLoaded) {
31 | SplashScreen.hideAsync();
32 | }
33 | }, [fontsLoaded]);
34 |
35 | return (
36 |
37 | }>
38 |
39 |
40 |
41 | );
42 | }
43 |
44 | function Main(): JSX.Element {
45 | return (
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 | );
55 | }
56 |
57 | export default Main;
58 |
--------------------------------------------------------------------------------
/src/navigation/permissoned_navigator.tsx:
--------------------------------------------------------------------------------
1 | import { useCallback } from 'react';
2 | import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
3 | import { defaultTheme } from '@/themes';
4 | import { PermissionedParamsList, ScreenTypes } from '@/configs/screen_types';
5 | import IcTabArchives from '@assets/icons/ic_tab_archives.svg';
6 | import IcTabArchivesActive from '@assets/icons/ic_tab_archives_active.svg';
7 | import IcTabHome from '@assets/icons/ic_tab_home.svg';
8 | import IcTabHomeActive from '@assets/icons/ic_tab_home_active.svg';
9 | import IcTabSettings from '@assets/icons/ic_tab_settings.svg';
10 | import IcTabSettingsActive from '@assets/icons/ic_tab_settings_active.svg';
11 | import { useLocationSocket } from '@/hooks/use_location_socket';
12 | import { ArchivesNavigator } from './tabs/archives_navigator';
13 | import { HomeNavigator } from './tabs/home_navigator';
14 | import { SettingssNavigator } from './tabs/settings_navigator';
15 |
16 | const Tab = createBottomTabNavigator();
17 |
18 | export function PermissionedNavigator(): JSX.Element {
19 | useLocationSocket();
20 |
21 | const renderTabArchivesIcon = useCallback((focused: boolean, size: number): JSX.Element => {
22 | return focused ? : ;
23 | }, []);
24 |
25 | const renderTabHomeIcon = useCallback((focused: boolean, size: number): JSX.Element => {
26 | return focused ? : ;
27 | }, []);
28 |
29 | const renderTabSettingsIcon = useCallback((focused: boolean, size: number): JSX.Element => {
30 | return focused ? : ;
31 | }, []);
32 |
33 | return (
34 |
35 | renderTabArchivesIcon(focused, 28),
43 | tabBarHideOnKeyboard: true,
44 | }}
45 | />
46 | renderTabHomeIcon(focused, 28),
54 | tabBarHideOnKeyboard: true,
55 | }}
56 | />
57 | renderTabSettingsIcon(focused, 28),
65 | }}
66 | />
67 |
68 | );
69 | }
70 |
--------------------------------------------------------------------------------
/src/navigation/root_navigator.tsx:
--------------------------------------------------------------------------------
1 | import Toast from 'react-native-toast-message';
2 | import { enableScreens } from 'react-native-screens';
3 | import { useRecoilValue } from 'recoil';
4 | import { NavigationContainer } from '@react-navigation/native';
5 | import { createStackNavigator } from '@react-navigation/stack';
6 | import { ScreenTypes, RootStackParamList } from '@/configs/screen_types';
7 | import { settingsAtom } from '@/recoils/settings_states';
8 | import { PermissionedNavigator } from './permissoned_navigator';
9 | import { UnPermissionedNavigator } from './un_permissoned_navigator';
10 |
11 | enableScreens();
12 |
13 | const RootStack = createStackNavigator();
14 |
15 | export function RootNavigator(): JSX.Element {
16 | const settings = useRecoilValue(settingsAtom);
17 |
18 | return (
19 |
20 |
21 | {!settings.hasPermissions ? (
22 |
27 | ) : (
28 |
33 | )}
34 |
35 |
36 |
37 | );
38 | }
39 |
--------------------------------------------------------------------------------
/src/navigation/tabs/archives_navigator.tsx:
--------------------------------------------------------------------------------
1 | import { createStackNavigator } from '@react-navigation/stack';
2 | import { ScreenTypes, ArchivesParamsList } from '@/configs/screen_types';
3 | import ArchivesScreen from '@/screens/archives';
4 | import PostChatScreen from '@/screens/postChat';
5 | import PostDetailScreen from '@/screens/postDetail';
6 | import PostWriteScreen from '@/screens/postWrite';
7 |
8 | const Main = createStackNavigator();
9 |
10 | export function ArchivesNavigator(): JSX.Element {
11 | return (
12 |
13 |
14 |
15 |
16 |
17 |
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/src/navigation/tabs/home_navigator.tsx:
--------------------------------------------------------------------------------
1 | import { createStackNavigator } from '@react-navigation/stack';
2 | import { ScreenTypes, HomeParamsList } from '@/configs/screen_types';
3 | import HomeScreen from '@/screens/home';
4 | import PostChatScreen from '@/screens/postChat';
5 | import PostDetailScreen from '@/screens/postDetail';
6 | import PostWriteScreen from '@/screens/postWrite';
7 |
8 | const Main = createStackNavigator();
9 |
10 | export function HomeNavigator(): JSX.Element {
11 | return (
12 |
13 |
14 |
15 |
16 |
17 |
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/src/navigation/tabs/settings_navigator.tsx:
--------------------------------------------------------------------------------
1 | import { createStackNavigator } from '@react-navigation/stack';
2 | import { ScreenTypes, SettingsParamsList } from '@/configs/screen_types';
3 | import InfoDevelopersScreen from '@/screens/infoDevelopers';
4 | import SettingsScreen from '@/screens/settings';
5 |
6 | const Main = createStackNavigator();
7 |
8 | export function SettingssNavigator(): JSX.Element {
9 | return (
10 |
11 |
12 |
13 |
14 | );
15 | }
16 |
--------------------------------------------------------------------------------
/src/navigation/un_permissoned_navigator.tsx:
--------------------------------------------------------------------------------
1 | import { createStackNavigator } from '@react-navigation/stack';
2 | import { ScreenTypes, UnPermissionedParamsList } from '@/configs/screen_types';
3 | import OnBoardingScreen from '@/screens/onBoarding';
4 |
5 | const Main = createStackNavigator();
6 |
7 | export function UnPermissionedNavigator(): JSX.Element {
8 | return (
9 |
10 |
11 |
12 | );
13 | }
14 |
--------------------------------------------------------------------------------
/src/recoils/location_states.ts:
--------------------------------------------------------------------------------
1 | import { atom } from 'recoil';
2 |
3 | // ## -------- LocationStates -------- ##
4 | export interface LocationStates {
5 | latitude: number;
6 | longitude: number;
7 | }
8 |
9 | export const defaultLocation: LocationStates = {
10 | latitude: 0,
11 | longitude: 0,
12 | };
13 |
14 | export const locationAtom = atom({
15 | key: 'locationState',
16 | default: defaultLocation,
17 | });
18 |
--------------------------------------------------------------------------------
/src/recoils/settings_states.ts:
--------------------------------------------------------------------------------
1 | import { atom } from 'recoil';
2 | import { RecoilStorageKeys } from '@/configs/recoil_keys';
3 | import { persistAtom } from '@/utils/recoil_utils';
4 |
5 | export interface SettingsStates {
6 | hasPermissions: boolean;
7 | }
8 |
9 | export const defaultSettings: SettingsStates = {
10 | hasPermissions: false,
11 | };
12 |
13 | export const settingsAtom = atom({
14 | key: 'settingsState',
15 | default: defaultSettings,
16 | effects_UNSTABLE: [persistAtom(RecoilStorageKeys.SETTINGS, defaultSettings)],
17 | });
18 |
--------------------------------------------------------------------------------
/src/screens/archives/archiveItem/index.tsx:
--------------------------------------------------------------------------------
1 | import DayJS from 'dayjs';
2 | import { memo, useCallback } from 'react';
3 | import IsEqual from 'react-fast-compare';
4 | import { AnimatedImage, Text, TouchableOpacity, View } from 'react-native-ui-lib';
5 | import { BottomTabNavigationProp } from '@react-navigation/bottom-tabs';
6 | import { useNavigation, CompositeNavigationProp } from '@react-navigation/native';
7 | import { StackNavigationProp } from '@react-navigation/stack';
8 | import IcChat from '@assets/icons/ic_chat.svg';
9 | import { ArchivesParamsList, PermissionedParamsList, ScreenTypes } from '@/configs/screen_types';
10 | import { PostPreviewDTO } from '@/dtos/post_dtos';
11 | import { styles } from './styles';
12 |
13 | type ScreenNavigationProps = CompositeNavigationProp<
14 | BottomTabNavigationProp,
15 | StackNavigationProp
16 | >;
17 |
18 | type ArchiveItemProps = {
19 | item: PostPreviewDTO;
20 | };
21 | function ArchiveItem(props: ArchiveItemProps): JSX.Element {
22 | const { item } = props;
23 | const navigation = useNavigation();
24 |
25 | const onPress = useCallback((): void => {
26 | navigation.navigate(ScreenTypes.SHARED_POST_DETAIL, {
27 | post: item,
28 | });
29 | }, []);
30 |
31 | return (
32 |
33 |
34 |
35 |
42 |
43 |
44 | {item.title}
45 |
46 |
47 | {item.contentsSnippet}
48 |
49 |
50 |
51 | {item.distanceM}M
52 | {` · ${DayJS().diff(item.createdAt, 'hour')}시간 전`}
53 |
54 |
55 |
56 | {item.joinedUsers}
57 |
58 |
59 |
60 |
61 |
62 |
63 | );
64 | }
65 |
66 | export default memo(ArchiveItem, IsEqual);
67 |
--------------------------------------------------------------------------------
/src/screens/archives/archiveItem/styles.ts:
--------------------------------------------------------------------------------
1 | import { StyleSheet } from 'react-native';
2 | import { defaultTheme } from '@/themes';
3 |
4 | export const styles = StyleSheet.create({
5 | wrapper: {
6 | marginHorizontal: 24,
7 | marginVertical: 8,
8 | backgroundColor: 'white',
9 | borderRadius: 12,
10 | height: 120,
11 | shadowColor: 'black',
12 | // iOS
13 | shadowOffset: { width: 0, height: 2 },
14 | shadowOpacity: 0.12,
15 | shadowRadius: 2,
16 |
17 | // Android
18 | elevation: 2,
19 | },
20 | container: {
21 | flex: 1,
22 | flexDirection: 'row',
23 | margin: 10,
24 | },
25 | thumbnailImage: {
26 | width: 100,
27 | height: 100,
28 | borderRadius: 12,
29 | },
30 | contentsContainer: {
31 | flex: 1,
32 | marginLeft: 12,
33 | justifyContent: 'space-between',
34 | },
35 | title: {
36 | fontSize: 18,
37 | color: defaultTheme.titleDefault,
38 | fontFamily: 'PretendardBold',
39 | },
40 | contents: {
41 | fontSize: 16,
42 | paddingTop: 4,
43 | color: defaultTheme.descDefault,
44 | fontFamily: 'PretendardRegular',
45 | },
46 | captionContainer: {
47 | flexDirection: 'row',
48 | justifyContent: 'space-between',
49 | },
50 | captionContentContainer: {
51 | flexDirection: 'row',
52 | },
53 | distance: {
54 | fontSize: 14,
55 | color: defaultTheme.primary,
56 | fontFamily: 'PretendardBold',
57 | },
58 | captionLabel: {
59 | fontSize: 14,
60 | color: defaultTheme.captionDefault,
61 | fontFamily: 'PretendardRegular',
62 | },
63 | });
64 |
--------------------------------------------------------------------------------
/src/screens/archives/index.tsx:
--------------------------------------------------------------------------------
1 | import { memo, useCallback, useEffect } from 'react';
2 | import IsEqual from 'react-fast-compare';
3 | import { FlatList, Text, View } from 'react-native';
4 | import { SafeAreaView } from 'react-native-safe-area-context';
5 | import { useQuery } from 'react-query';
6 | import { useRecoilValue } from 'recoil';
7 | import { BottomTabNavigationProp } from '@react-navigation/bottom-tabs';
8 | import { useNavigation, CompositeNavigationProp } from '@react-navigation/native';
9 | import { StackNavigationProp } from '@react-navigation/stack';
10 | import CustomHeader from '@/components/customHeader';
11 | import EmptyScreen from '@/components/emptyScreen';
12 | import FloatingButton from '@/components/floatingButton';
13 | import { ArchivesParamsList, PermissionedParamsList, ScreenTypes } from '@/configs/screen_types';
14 | import { queryKeys } from '@/configs/api_keys';
15 | import { translate } from '@/i18n';
16 | import { getPostsByDistance } from '@/services/posts_service';
17 | import { locationAtom } from '@/recoils/location_states';
18 | import ArchiveItem from './archiveItem';
19 | import { styles } from './styles';
20 |
21 | type ScreenNavigationProps = CompositeNavigationProp<
22 | BottomTabNavigationProp,
23 | StackNavigationProp
24 | >;
25 |
26 | function ArchivesScreen(): JSX.Element {
27 | const locations = useRecoilValue(locationAtom);
28 | const navigation = useNavigation();
29 | const {
30 | isLoading,
31 | error,
32 | data: postsData,
33 | refetch,
34 | } = useQuery(queryKeys.postsByDistance, () => getPostsByDistance(locations.latitude, locations.longitude));
35 |
36 | useEffect(() => {
37 | refetch();
38 | }, [locations]);
39 |
40 | const onFABPress = useCallback((): void => {
41 | navigation.navigate(ScreenTypes.SHARED_POST_WRITE);
42 | }, []);
43 |
44 | const renderItem = useCallback(({ item }) => , []);
45 |
46 | if (isLoading || !postsData) {
47 | return (
48 |
49 | Loading ...
50 |
51 | );
52 | }
53 | if (error) {
54 | return (
55 |
56 | Error!!
57 |
58 | );
59 | }
60 | return (
61 |
62 |
63 | {postsData.length < 1 ? (
64 |
65 | ) : (
66 | item.id} />
67 | )}
68 |
69 |
70 | );
71 | }
72 |
73 | export default memo(ArchivesScreen, IsEqual);
74 |
--------------------------------------------------------------------------------
/src/screens/archives/styles.ts:
--------------------------------------------------------------------------------
1 | import { StyleSheet } from 'react-native';
2 | import { defaultTheme } from '@/themes';
3 |
4 | export const styles = StyleSheet.create({
5 | wrapper: {
6 | flex: 1,
7 | backgroundColor: defaultTheme.backgroundDefault,
8 | },
9 | });
10 |
--------------------------------------------------------------------------------
/src/screens/home/custom_map_styles.ts:
--------------------------------------------------------------------------------
1 | import { MapStyleElement } from 'react-native-maps';
2 |
3 | export const customMapStyles: MapStyleElement[] = [
4 | {
5 | elementType: 'geometry',
6 | stylers: [
7 | {
8 | color: '#242f3e',
9 | },
10 | ],
11 | },
12 | {
13 | elementType: 'labels.text.fill',
14 | stylers: [
15 | {
16 | color: '#746855',
17 | },
18 | ],
19 | },
20 | {
21 | elementType: 'labels.text.stroke',
22 | stylers: [
23 | {
24 | color: '#242f3e',
25 | },
26 | ],
27 | },
28 | {
29 | featureType: 'administrative.locality',
30 | elementType: 'labels.text.fill',
31 | stylers: [
32 | {
33 | color: '#d59563',
34 | },
35 | ],
36 | },
37 | {
38 | featureType: 'poi',
39 | elementType: 'labels.text.fill',
40 | stylers: [
41 | {
42 | color: '#d59563',
43 | },
44 | ],
45 | },
46 | {
47 | featureType: 'poi.park',
48 | elementType: 'geometry',
49 | stylers: [
50 | {
51 | color: '#263c3f',
52 | },
53 | ],
54 | },
55 | {
56 | featureType: 'poi.park',
57 | elementType: 'labels.text.fill',
58 | stylers: [
59 | {
60 | color: '#6b9a76',
61 | },
62 | ],
63 | },
64 | {
65 | featureType: 'road',
66 | elementType: 'geometry',
67 | stylers: [
68 | {
69 | color: '#38414e',
70 | },
71 | ],
72 | },
73 | {
74 | featureType: 'road',
75 | elementType: 'geometry.stroke',
76 | stylers: [
77 | {
78 | color: '#212a37',
79 | },
80 | ],
81 | },
82 | {
83 | featureType: 'road',
84 | elementType: 'labels.text.fill',
85 | stylers: [
86 | {
87 | color: '#9ca5b3',
88 | },
89 | ],
90 | },
91 | {
92 | featureType: 'road.highway',
93 | elementType: 'geometry',
94 | stylers: [
95 | {
96 | color: '#746855',
97 | },
98 | ],
99 | },
100 | {
101 | featureType: 'road.highway',
102 | elementType: 'geometry.stroke',
103 | stylers: [
104 | {
105 | color: '#1f2835',
106 | },
107 | ],
108 | },
109 | {
110 | featureType: 'road.highway',
111 | elementType: 'labels.text.fill',
112 | stylers: [
113 | {
114 | color: '#f3d19c',
115 | },
116 | ],
117 | },
118 | {
119 | featureType: 'transit',
120 | elementType: 'geometry',
121 | stylers: [
122 | {
123 | color: '#2f3948',
124 | },
125 | ],
126 | },
127 | {
128 | featureType: 'transit.station',
129 | elementType: 'labels.text.fill',
130 | stylers: [
131 | {
132 | color: '#d59563',
133 | },
134 | ],
135 | },
136 | {
137 | featureType: 'water',
138 | elementType: 'geometry',
139 | stylers: [
140 | {
141 | color: '#17263c',
142 | },
143 | ],
144 | },
145 | {
146 | featureType: 'water',
147 | elementType: 'labels.text.fill',
148 | stylers: [
149 | {
150 | color: '#515c6d',
151 | },
152 | ],
153 | },
154 | {
155 | featureType: 'water',
156 | elementType: 'labels.text.stroke',
157 | stylers: [
158 | {
159 | color: '#17263c',
160 | },
161 | ],
162 | },
163 | ];
164 |
--------------------------------------------------------------------------------
/src/screens/home/index.tsx:
--------------------------------------------------------------------------------
1 | import { memo, useCallback, useEffect, useMemo } from 'react';
2 | import IsEqual from 'react-fast-compare';
3 | import { Text } from 'react-native';
4 | import MapView, { Marker } from 'react-native-maps';
5 | import { View } from 'react-native-ui-lib';
6 | import { useQuery } from 'react-query';
7 | import { useRecoilValue } from 'recoil';
8 | import { BottomTabNavigationProp } from '@react-navigation/bottom-tabs';
9 | import { useNavigation, CompositeNavigationProp } from '@react-navigation/native';
10 | import { StackNavigationProp } from '@react-navigation/stack';
11 | import FloatingButton from '@/components/floatingButton';
12 | import { queryKeys } from '@/configs/api_keys';
13 | import { HomeParamsList, PermissionedParamsList, ScreenTypes } from '@/configs/screen_types';
14 | import { PostPreviewDTO } from '@/dtos/post_dtos';
15 | import { getPostsByDistance } from '@/services/posts_service';
16 | import { locationAtom } from '@/recoils/location_states';
17 | import { customMapStyles } from './custom_map_styles';
18 | import { styles } from './styles';
19 |
20 | function getCurrentRegion(latitude: number, longitude: number) {
21 | return {
22 | latitude,
23 | longitude,
24 | latitudeDelta: 0.0222,
25 | longitudeDelta: 0.0222,
26 | };
27 | }
28 |
29 | type ScreenNavigationProps = CompositeNavigationProp<
30 | BottomTabNavigationProp,
31 | StackNavigationProp
32 | >;
33 |
34 | function HomeScreen(): JSX.Element {
35 | const navigation = useNavigation();
36 | const locations = useRecoilValue(locationAtom);
37 | const {
38 | isLoading,
39 | error,
40 | data: postsData,
41 | refetch,
42 | } = useQuery(queryKeys.postsByDistance, () => getPostsByDistance(locations.latitude, locations.longitude));
43 |
44 | useEffect(() => {
45 | refetch();
46 | }, [locations]);
47 |
48 | const curruentRegion = useMemo(
49 | () => getCurrentRegion(locations.latitude, locations.longitude),
50 | [locations.latitude, locations.longitude],
51 | );
52 |
53 | const getPostCoordinate = useCallback((latitude: number, longitude: number) => {
54 | return {
55 | latitude,
56 | longitude,
57 | };
58 | }, []);
59 |
60 | const onPostMarkerPress = useCallback((item: PostPreviewDTO): void => {
61 | navigation.navigate(ScreenTypes.SHARED_POST_DETAIL, {
62 | post: item,
63 | });
64 | }, []);
65 |
66 | const onFABPress = useCallback((): void => {
67 | navigation.navigate(ScreenTypes.SHARED_POST_WRITE);
68 | }, []);
69 |
70 | if (isLoading) {
71 | return Loading ...;
72 | }
73 | if (error) {
74 | return Error!!;
75 | }
76 | return (
77 |
78 |
88 | {postsData?.map((post) => (
89 | onPostMarkerPress(post)}
94 | />
95 | ))}
96 |
97 |
98 |
99 | );
100 | }
101 |
102 | export default memo(HomeScreen, IsEqual);
103 |
--------------------------------------------------------------------------------
/src/screens/home/styles.ts:
--------------------------------------------------------------------------------
1 | import { StyleSheet } from 'react-native';
2 |
3 | export const styles = StyleSheet.create({
4 | wrapper: {
5 | position: 'absolute',
6 | top: 0,
7 | left: 0,
8 | right: 0,
9 | bottom: 0,
10 | justifyContent: 'flex-end',
11 | alignItems: 'center',
12 | },
13 | mapWrapper: {
14 | position: 'absolute',
15 | top: 0,
16 | left: 0,
17 | right: 0,
18 | bottom: 0,
19 | },
20 | });
21 |
--------------------------------------------------------------------------------
/src/screens/infoDevelopers/index.tsx:
--------------------------------------------------------------------------------
1 | import { memo } from 'react';
2 | import IsEqual from 'react-fast-compare';
3 | import { SafeAreaView } from 'react-native-safe-area-context';
4 | import { Text, View } from 'react-native-ui-lib';
5 | import CustomAppBar from '@/components/customAppbar';
6 | import { styles } from './styles';
7 |
8 | function InfoDevelopers(): JSX.Element {
9 | return (
10 |
11 |
12 |
13 |
14 | {`
15 | - 이재왕
16 | - 지웅재 <93y0916@naver.com>
17 | - 이봉주
18 | `}
19 |
20 |
21 |
22 | );
23 | }
24 |
25 | export default memo(InfoDevelopers, IsEqual);
26 |
--------------------------------------------------------------------------------
/src/screens/infoDevelopers/styles.ts:
--------------------------------------------------------------------------------
1 | import { StyleSheet } from 'react-native';
2 | import { defaultTheme } from '@/themes';
3 |
4 | export const styles = StyleSheet.create({
5 | wrapper: {
6 | flex: 1,
7 | backgroundColor: 'white',
8 | },
9 | textWrapper: {
10 | padding: 24,
11 | },
12 | text: {
13 | fontSize: 16,
14 | color: defaultTheme.titleDefault,
15 | fontFamily: 'PretendardRegular',
16 | },
17 | });
18 |
--------------------------------------------------------------------------------
/src/screens/onBoarding/index.tsx:
--------------------------------------------------------------------------------
1 | import * as Location from 'expo-location';
2 | import LottieView from 'lottie-react-native';
3 | import { memo, useCallback } from 'react';
4 | import IsEqual from 'react-fast-compare';
5 | import { Alert } from 'react-native';
6 | import { Button, Text, View } from 'react-native-ui-lib';
7 | import { useRecoilState } from 'recoil';
8 | import OnboardLottie from '@assets/lotties/onboard.json';
9 | import { translate } from '@/i18n';
10 | import { settingsAtom } from '@/recoils/settings_states';
11 | import { styles } from './styles';
12 |
13 | function OnBoardingScreen(): JSX.Element {
14 | const [settings, setSettings] = useRecoilState(settingsAtom);
15 |
16 | const onAcceptPress = useCallback(async (): Promise => {
17 | const { status } = await Location.requestForegroundPermissionsAsync();
18 | if (status !== 'granted') {
19 | Alert.alert(translate('dialogs.permissionDeniedTitle'), translate('dialogs.permissionRequestLocation'), [
20 | { text: translate('common.ok') },
21 | ]);
22 | return;
23 | }
24 |
25 | setSettings({
26 | ...settings,
27 | hasPermissions: true,
28 | });
29 | }, []);
30 |
31 | return (
32 |
33 |
34 |
35 |
36 |
37 | {`
38 | 왁자지껄은 위치기반 SNS 입니다
39 | 사용자의 위치정보를 알 수 있도록 위치권한을 허용해 주세요
40 | 어떠한 사용자의 데이터도 수집되지 않습니다
41 | `}
42 |
43 |
44 |
51 |
52 | );
53 | }
54 |
55 | export default memo(OnBoardingScreen, IsEqual);
56 |
--------------------------------------------------------------------------------
/src/screens/onBoarding/styles.ts:
--------------------------------------------------------------------------------
1 | import { StyleSheet } from 'react-native';
2 | import { colors, defaultTheme } from '@/themes';
3 |
4 | export const styles = StyleSheet.create({
5 | wrapper: {
6 | flex: 1,
7 | backgroundColor: defaultTheme.primary,
8 | justifyContent: 'space-between',
9 | },
10 | dummy: {
11 | height: 60,
12 | },
13 | lottie: {
14 | alignSelf: 'center',
15 | width: 200,
16 | height: 200,
17 | backgroundColor: 'transparent',
18 | },
19 | label: {
20 | marginTop: -40,
21 | fontSize: 14,
22 | fontFamily: 'PretendardRegular',
23 | color: colors.gray400,
24 | lineHeight: 20,
25 | textAlign: 'center',
26 | },
27 | button: {
28 | marginHorizontal: 20,
29 | marginBottom: 32,
30 | borderRadius: 12,
31 | paddingVertical: 18,
32 | backgroundColor: 'white',
33 | },
34 | buttonLabel: {
35 | fontSize: 18,
36 | fontFamily: 'PretendardBold',
37 | color: defaultTheme.titleDefault,
38 | },
39 | });
40 |
--------------------------------------------------------------------------------
/src/screens/postChat/chatBalloon/balloon_left.tsx:
--------------------------------------------------------------------------------
1 | import DayJS from 'dayjs';
2 | import { memo } from 'react';
3 | import IsEqual from 'react-fast-compare';
4 | import { Text, View } from 'react-native-ui-lib';
5 | import { styles } from './styles';
6 |
7 | type BalloonLeftProps = {
8 | senderNickName: string;
9 | displayNickName?: boolean;
10 | message: string;
11 | createdAt: Date;
12 | };
13 |
14 | function BalloonLeft(props: BalloonLeftProps): JSX.Element {
15 | const { createdAt, displayNickName, message, senderNickName } = props;
16 |
17 | return (
18 |
19 | {displayNickName && {senderNickName}}
20 |
21 |
22 | {message}
23 |
24 | {DayJS(createdAt).format('A hh:mm')}
25 |
26 |
27 | );
28 | }
29 |
30 | BalloonLeft.defaultProps = {
31 | displayNickName: true,
32 | } as BalloonLeftProps;
33 |
34 | export default memo(BalloonLeft, IsEqual);
35 |
--------------------------------------------------------------------------------
/src/screens/postChat/chatBalloon/balloon_right.tsx:
--------------------------------------------------------------------------------
1 | import DayJS from 'dayjs';
2 | import { memo } from 'react';
3 | import IsEqual from 'react-fast-compare';
4 | import { Text, View } from 'react-native-ui-lib';
5 | import { styles } from './styles';
6 |
7 | type BalloonRightProps = {
8 | message: string;
9 | createdAt: Date;
10 | };
11 |
12 | function BalloonRight(props: BalloonRightProps): JSX.Element {
13 | const { createdAt, message } = props;
14 |
15 | return (
16 |
17 | {DayJS(createdAt).format('A hh:mm')}
18 |
19 | {message}
20 |
21 |
22 | );
23 | }
24 |
25 | export default memo(BalloonRight, IsEqual);
26 |
--------------------------------------------------------------------------------
/src/screens/postChat/chatBalloon/index.tsx:
--------------------------------------------------------------------------------
1 | import { memo, useMemo } from 'react';
2 | import IsEqual from 'react-fast-compare';
3 | import BalloonLeft from './balloon_left';
4 | import BalloonRight from './balloon_right';
5 |
6 | type ChatBalloonProps = {
7 | senderNickName: string;
8 | currentNickName: string;
9 | displaySenderNickName?: boolean;
10 | message: string;
11 | createdAt: Date;
12 | };
13 |
14 | function ChatBalloon(props: ChatBalloonProps): JSX.Element {
15 | const { createdAt, currentNickName, displaySenderNickName, message, senderNickName } = props;
16 |
17 | const isReceived = useMemo(() => currentNickName !== senderNickName, []);
18 |
19 | return isReceived ? (
20 |
26 | ) : (
27 |
28 | );
29 | }
30 |
31 | ChatBalloon.defaultProps = {
32 | displaySenderNickName: true,
33 | } as ChatBalloonProps;
34 |
35 | export default memo(ChatBalloon, IsEqual);
36 |
--------------------------------------------------------------------------------
/src/screens/postChat/chatBalloon/styles.ts:
--------------------------------------------------------------------------------
1 | import { Dimensions, StyleSheet } from 'react-native';
2 | import { colors, defaultTheme } from '@/themes';
3 |
4 | export const styles = StyleSheet.create({
5 | balloon: {
6 | flexDirection: 'row',
7 | alignItems: 'center',
8 | paddingVertical: 6,
9 | },
10 | me: {
11 | justifyContent: 'flex-end',
12 | },
13 | others: {
14 | justifyContent: 'flex-start',
15 | },
16 | cloudContainer: {
17 | flexDirection: 'row',
18 | alignItems: 'center',
19 | justifyContent: 'center',
20 | padding: 8,
21 | borderRadius: 8,
22 | maxWidth: Dimensions.get('window').width - 120,
23 | },
24 | cloudLeft: {
25 | backgroundColor: colors.gray300,
26 | marginRight: 4,
27 | },
28 | cloudRight: {
29 | backgroundColor: defaultTheme.primary,
30 | marginLeft: 4,
31 | },
32 | nickName: {
33 | paddingTop: 6,
34 | fontSize: 14,
35 | fontFamily: 'PretendardBold',
36 | },
37 | messageLabel: {
38 | fontSize: 14,
39 | fontFamily: 'PretendardRegular',
40 | },
41 | messageOthers: {
42 | color: defaultTheme.titleDefault,
43 | },
44 | messageMe: {
45 | color: 'white',
46 | },
47 | dateLabel: {
48 | fontSize: 12,
49 | color: defaultTheme.captionDefault,
50 | fontFamily: 'PretendardRegular',
51 | },
52 | });
53 |
--------------------------------------------------------------------------------
/src/screens/postChat/index.tsx:
--------------------------------------------------------------------------------
1 | import DayJS from 'dayjs';
2 | import { memo, useCallback, useMemo, useRef, useState } from 'react';
3 | import IsEqual from 'react-fast-compare';
4 | import { FlatList, ListRenderItemInfo } from 'react-native';
5 | import { SafeAreaView } from 'react-native-safe-area-context';
6 | import { Button, Incubator, Text } from 'react-native-ui-lib';
7 | import { useRoute, RouteProp } from '@react-navigation/native';
8 | import IcSend from '@assets/icons/ic_send.svg';
9 | import CustomAppBar from '@/components/customAppbar';
10 | import KeyboardAwareScrollView from '@/components/keyboardAwareScrollView';
11 | import { ArchivesParamsList, ScreenTypes } from '@/configs/screen_types';
12 | import { ChatPubDestination } from '@/configs/socket_keys';
13 | import { JoinedNewMember, LeavedExistingMember, SendMessageRequest, SentMessage } from '@/dtos/chat_dtos';
14 | import { useChatSocket } from '@/hooks/use_chat_socket';
15 | import { translate } from '@/i18n';
16 | import ChatBalloon from './chatBalloon';
17 | import { MessageState } from './interfaces';
18 | import { styles } from './styles';
19 |
20 | const { TextField } = Incubator;
21 |
22 | type ScreenRouteProps = RouteProp;
23 |
24 | function PostChatScreen(): JSX.Element {
25 | const route = useRoute();
26 | const messagesRef = useRef(null);
27 | const [messages, setMessages] = useState([]);
28 | const [text, setText] = useState('');
29 |
30 | const handleSubNewMessage = useCallback((res: SentMessage) => {
31 | setMessages((prev) => [
32 | ...prev,
33 | { nickName: res.nickName, message: res.message, createdAt: res.createdAt } as MessageState,
34 | ]);
35 | }, []);
36 |
37 | const handleNewMemberJoined = useCallback((res: JoinedNewMember) => {
38 | setMessages((prev) => [
39 | ...prev,
40 | { nickName: '알림', message: `${res.nickName} 님이 입장하셨습니다.`, createdAt: res.joinedAt } as MessageState,
41 | ]);
42 | }, []);
43 |
44 | const handleMemberLeaved = useCallback((res: LeavedExistingMember) => {
45 | setMessages((prev) => [
46 | ...prev,
47 | { nickName: '알림', message: `${res.nickName} 님이 퇴장하셨습니다.`, createdAt: res.leavedAt } as MessageState,
48 | ]);
49 | }, []);
50 |
51 | const handleSocketDisconnect = useCallback(() => {
52 | setMessages([]);
53 | }, []);
54 |
55 | const { chatSocket, nickName } = useChatSocket({
56 | roomId: route.params.post.id,
57 | subNewMemberJoined: handleNewMemberJoined,
58 | subMemberLeaved: handleMemberLeaved,
59 | subNewMessage: handleSubNewMessage,
60 | handleDisconnect: handleSocketDisconnect,
61 | });
62 |
63 | const onSendClick = useCallback((): void => {
64 | if (!chatSocket || !nickName || !text) {
65 | return;
66 | }
67 |
68 | chatSocket?.emit(ChatPubDestination.SEND_MESSAGE, {
69 | roomId: route.params.post.id,
70 | nickName,
71 | message: text,
72 | } as SendMessageRequest);
73 | setText('');
74 | }, [chatSocket, text]);
75 |
76 | const onMessageListUpdated = useCallback((): void => {
77 | messagesRef.current?.scrollToEnd({ animated: true });
78 | }, [messagesRef.current]);
79 |
80 | const getLastMessage = useMemo(() => messages.pop(), []);
81 |
82 | const renderMessages = useCallback(
83 | (info: ListRenderItemInfo): JSX.Element => {
84 | return (
85 |
91 | );
92 | },
93 | [nickName],
94 | );
95 |
96 | if (!chatSocket || !nickName) {
97 | return Loading ...;
98 | }
99 | return (
100 |
101 |
102 |
103 | <>
104 | DayJS(item.createdAt).valueOf().toString()}
110 | onContentSizeChange={onMessageListUpdated}
111 | onLayout={onMessageListUpdated}
112 | />
113 |
122 | }
123 | />
124 | >
125 |
126 |
127 | );
128 | }
129 |
130 | export default memo(PostChatScreen, IsEqual);
131 |
--------------------------------------------------------------------------------
/src/screens/postChat/interfaces.ts:
--------------------------------------------------------------------------------
1 | export interface MessageState {
2 | nickName: string;
3 | message: string;
4 | createdAt: Date;
5 | }
6 |
--------------------------------------------------------------------------------
/src/screens/postChat/styles.ts:
--------------------------------------------------------------------------------
1 | import { StyleSheet } from 'react-native';
2 | import { colors } from '@/themes';
3 |
4 | export const styles = StyleSheet.create({
5 | wrapper: {
6 | flex: 1,
7 | backgroundColor: 'white',
8 | },
9 | messageListContent: {
10 | marginHorizontal: 20,
11 | },
12 | inputContainer: {
13 | backgroundColor: colors.gray300,
14 | },
15 | inputfieldContainer: {
16 | paddingLeft: 12,
17 | },
18 | buttonSend: {
19 | width: 60,
20 | height: 60,
21 | borderRadius: 0,
22 | },
23 | });
24 |
--------------------------------------------------------------------------------
/src/screens/postDetail/index.tsx:
--------------------------------------------------------------------------------
1 | import DayJS from 'dayjs';
2 | import { memo, useCallback } from 'react';
3 | import IsEqual from 'react-fast-compare';
4 | import { ActivityIndicator, ScrollView } from 'react-native';
5 | import { useSafeAreaInsets, SafeAreaView } from 'react-native-safe-area-context';
6 | import { Avatar, AnimatedImage, Button, Text, View } from 'react-native-ui-lib';
7 | import { useQuery } from 'react-query';
8 | import { BottomTabNavigationProp } from '@react-navigation/bottom-tabs';
9 | import { useNavigation, useRoute, CompositeNavigationProp, RouteProp } from '@react-navigation/native';
10 | import { StackNavigationProp } from '@react-navigation/stack';
11 | import IcChat from '@assets/icons/ic_chat.svg';
12 | import { APPBAR_HEIGHT } from '@/themes';
13 | import CustomAppBar from '@/components/customAppbar';
14 | import { queryKeys } from '@/configs/api_keys';
15 | import { ArchivesParamsList, PermissionedParamsList, ScreenTypes } from '@/configs/screen_types';
16 | import { getPostById } from '@/services/posts_service';
17 | import { styles } from './styles';
18 |
19 | type ScreenNavigationProps = CompositeNavigationProp<
20 | BottomTabNavigationProp,
21 | StackNavigationProp
22 | >;
23 |
24 | type ScreenRouteProps = RouteProp;
25 |
26 | function PostDetailScreen(): JSX.Element {
27 | const route = useRoute();
28 | const navigation = useNavigation();
29 | const { post } = route.params;
30 | const insets = useSafeAreaInsets();
31 | const {
32 | isLoading,
33 | error,
34 | data: postData,
35 | } = useQuery(`${queryKeys.postsByDistance}_${post.id}`, () => getPostById(post.id));
36 |
37 | const onGoToChatPress = useCallback((): void => {
38 | navigation.navigate(ScreenTypes.SHARED_POST_CHAT, { post });
39 | }, []);
40 |
41 | if (isLoading || !postData) {
42 | return (
43 |
44 | Loading ...
45 |
46 | );
47 | }
48 | if (error) {
49 | return (
50 |
51 | Error!!
52 |
53 | );
54 | }
55 | return (
56 |
57 |
58 |
59 | }
63 | />
64 |
65 |
66 |
71 | 테스트 닉네임
72 |
73 |
74 | {post.distanceM}M
75 | {` · ${DayJS().diff(postData.createdAt, 'hour')}시간 전`}
76 |
77 |
78 |
79 |
80 | {postData.title}
81 | {postData.contents}
82 |
83 |
84 |
85 |
86 |
87 | {`현재 ${postData.joinedUsers}명이 채팅방에 참여 중입니다.`}
88 |
89 |
96 |
97 |
98 | );
99 | }
100 |
101 | export default memo(PostDetailScreen, IsEqual);
102 |
--------------------------------------------------------------------------------
/src/screens/postDetail/interfaces.ts:
--------------------------------------------------------------------------------
1 | export interface MessageState {
2 | nickName: string;
3 | message: string;
4 | }
5 |
--------------------------------------------------------------------------------
/src/screens/postDetail/styles.ts:
--------------------------------------------------------------------------------
1 | import { StyleSheet } from 'react-native';
2 | import { defaultTheme } from '@/themes';
3 |
4 | const PADDING_HORIZONTAL = 20;
5 |
6 | export const styles = StyleSheet.create({
7 | wrapper: {
8 | flex: 1,
9 | backgroundColor: 'white',
10 | },
11 | thumbnail: {
12 | resizeMode: 'cover',
13 | height: 250,
14 | },
15 | avatarContainer: {
16 | flexDirection: 'row',
17 | justifyContent: 'space-between',
18 | paddingVertical: 20,
19 | paddingHorizontal: PADDING_HORIZONTAL,
20 | },
21 | divider: {
22 | height: 1,
23 | backgroundColor: '#DDDDDD',
24 | marginHorizontal: PADDING_HORIZONTAL,
25 | },
26 | avatarName: {
27 | fontSize: 16,
28 | paddingLeft: 8,
29 | color: defaultTheme.titleDefault,
30 | fontFamily: 'PretendardBold',
31 | },
32 | profileContainer: {
33 | flexDirection: 'row',
34 | alignItems: 'center',
35 | },
36 | captionContainer: {
37 | flexDirection: 'row',
38 | alignItems: 'center',
39 | },
40 | distance: {
41 | fontSize: 14,
42 | color: defaultTheme.primary,
43 | fontFamily: 'PretendardBold',
44 | },
45 | createdTime: {
46 | fontSize: 14,
47 | color: defaultTheme.captionDefault,
48 | fontFamily: 'PretendardRegular',
49 | },
50 | contentContainer: {
51 | paddingVertical: 12,
52 | paddingHorizontal: PADDING_HORIZONTAL,
53 | },
54 | title: {
55 | fontSize: 18,
56 | color: defaultTheme.titleDefault,
57 | fontFamily: 'PretendardBold',
58 | paddingBottom: 12,
59 | },
60 | contents: {
61 | fontSize: 16,
62 | paddingTop: 4,
63 | color: defaultTheme.descDark,
64 | fontFamily: 'PretendardRegular',
65 | },
66 | gotoChatContainer: {
67 | position: 'absolute',
68 | bottom: 0,
69 | width: '100%',
70 | flexDirection: 'row',
71 | justifyContent: 'space-between',
72 | alignItems: 'center',
73 | paddingHorizontal: PADDING_HORIZONTAL,
74 | paddingVertical: 18,
75 | borderTopColor: '#DDDDDD',
76 | borderBottomColor: '#DDDDDD',
77 | borderTopWidth: 1,
78 | borderBottomWidth: 1,
79 | },
80 | chatDescContainer: {
81 | flexDirection: 'row',
82 | },
83 | chatButton: {
84 | width: 98,
85 | height: 32,
86 | },
87 | chatButtonLabel: {
88 | fontSize: 12,
89 | fontFamily: 'PretendardRegular',
90 | },
91 | });
92 |
--------------------------------------------------------------------------------
/src/screens/postWrite/index.tsx:
--------------------------------------------------------------------------------
1 | import * as ImagePicker from 'expo-image-picker';
2 | import { memo, useCallback, useEffect, useState } from 'react';
3 | import IsEqual from 'react-fast-compare';
4 | import { ActivityIndicator, Alert, Image } from 'react-native';
5 | import { SafeAreaView } from 'react-native-safe-area-context';
6 | import { AnimatedImage, Badge, Incubator, Text, View, TouchableOpacity } from 'react-native-ui-lib';
7 | import { useRecoilValue } from 'recoil';
8 | import { BottomTabNavigationProp } from '@react-navigation/bottom-tabs';
9 | import { useNavigation, CompositeNavigationProp } from '@react-navigation/native';
10 | import { StackNavigationProp } from '@react-navigation/stack';
11 | import IcClose from '@assets/icons/ic_close.png';
12 | import ThumbnailPlaceholder from '@assets/images/thumbnail_placeholder_upload.png';
13 | import CustomAppBar from '@/components/customAppbar';
14 | import Divider from '@/components/divider';
15 | import KeyboardAwareScrollView from '@/components/keyboardAwareScrollView';
16 | import { ArchivesParamsList, PermissionedParamsList } from '@/configs/screen_types';
17 | import { CreatePostRequest } from '@/dtos/post_dtos';
18 | import { locationAtom } from '@/recoils/location_states';
19 | import { createPost } from '@/services/posts_service';
20 | import { uploadThumbnail } from '@/services/upload_service';
21 | import { getDeviceInfo } from '@/helpers/device_utils';
22 | import { translate } from '@/i18n';
23 | import { defaultTheme } from '@/themes';
24 | import { initThumbnail, Thumbnail } from './interfaces';
25 | import { styles } from './styles';
26 |
27 | const { TextField } = Incubator;
28 |
29 | type ScreenNavigationProps = CompositeNavigationProp<
30 | BottomTabNavigationProp,
31 | StackNavigationProp
32 | >;
33 |
34 | function PostWrite(): JSX.Element {
35 | const locations = useRecoilValue(locationAtom);
36 | const navigation = useNavigation();
37 | const [thumbnail, setThumbnail] = useState(initThumbnail);
38 | const [uploadPercentage, setPercentage] = useState(0);
39 | const [title, setTitle] = useState('');
40 | const [contents, setContents] = useState('');
41 | const [isLoading, setLoading] = useState(false);
42 |
43 | useEffect(() => {
44 | return () => {
45 | setThumbnail(initThumbnail);
46 | setPercentage(0);
47 | setTitle('');
48 | setContents('');
49 | };
50 | }, []);
51 |
52 | const handleImagePicked = useCallback(async (pickerResult: ImagePicker.ImagePickerResult): Promise => {
53 | try {
54 | if (pickerResult.cancelled) {
55 | return;
56 | }
57 |
58 | // Handle 'percentage' with 'axios'
59 | setPercentage(20);
60 | const uploadResponse = await uploadThumbnail(pickerResult.uri, setPercentage);
61 | if (uploadResponse) {
62 | setThumbnail({ thumbnailUrl: uploadResponse.thumbnailUrl, originalFileName: uploadResponse.origianlFileName });
63 | } else {
64 | Alert.alert(translate('dialogs.uploadErrorTitle'), translate('dialogs.uploadErrorExceed'), [
65 | { text: translate('common.ok') },
66 | ]);
67 | }
68 | } catch (e) {
69 | Alert.alert(
70 | translate('dialogs.uploadErrorTitle'),
71 | 'Error occured when uploading image. Try again later please.',
72 | [{ text: translate('common.ok') }],
73 | );
74 | } finally {
75 | setPercentage(0);
76 | }
77 | }, []);
78 |
79 | const onThumbnailPress = useCallback(async (): Promise => {
80 | const { status } = await ImagePicker.requestMediaLibraryPermissionsAsync();
81 | if (status !== 'granted') {
82 | Alert.alert(translate('dialogs.permissionDeniedTitle'), translate('dialogs.permissionRequestPhotos'), [
83 | { text: translate('common.ok') },
84 | ]);
85 | return;
86 | }
87 |
88 | const result = await ImagePicker.launchImageLibraryAsync({
89 | mediaTypes: ImagePicker.MediaTypeOptions.Images,
90 | aspect: [1, 1],
91 | quality: 1,
92 | });
93 |
94 | handleImagePicked(result);
95 | }, []);
96 |
97 | const renderRightBarItem = useCallback((): JSX.Element => {
98 | const onWritePress = async (): Promise => {
99 | if (isLoading) {
100 | return;
101 | }
102 | const device = await getDeviceInfo();
103 | if (!device) {
104 | return;
105 | }
106 |
107 | setLoading(true);
108 | try {
109 | const req: CreatePostRequest = {
110 | deviceType: device.deviceType,
111 | deviceId: device.deviceId,
112 | latitude: locations.latitude,
113 | longitude: locations.longitude,
114 | title,
115 | contents,
116 | thumbnailUrl: thumbnail.thumbnailUrl,
117 | originalFileName: thumbnail.originalFileName,
118 | };
119 | const res = await createPost(req);
120 | if (res) {
121 | Alert.alert(translate('dialogs.postCreatingSucceedTitle'), translate('dialogs.postCreatingSucceed'));
122 | navigation.goBack();
123 | } else {
124 | Alert.alert(translate('dialogs.postCreatingFailedTitle'), translate('dialogs.postCreatingFailed'));
125 | }
126 | } catch (e) {
127 | Alert.alert(translate('dialogs.postCreatingFailedTitle'), translate('dialogs.postCreatingFailed'));
128 | } finally {
129 | setLoading(false);
130 | }
131 | };
132 |
133 | return (
134 |
135 | 완료
136 |
137 | );
138 | }, [locations, title, contents, thumbnail]);
139 |
140 | return (
141 |
142 |
143 | <>
144 |
145 |
146 |
147 |
148 | {thumbnail.thumbnailUrl ? (
149 |
150 |
154 |
161 |
162 | ) : (
163 |
164 | )}
165 | {uploadPercentage !== 0 ? : null}
166 |
167 |
168 |
169 |
179 |
180 |
191 |
192 | {`
193 | 휴대폰의 위치 기반으로 소식이 전해지며,
194 | 게시글은 작성일로부터 24시간 후에 자동으로 삭제됩니다.
195 |
196 | 개인정보가 노출되지 않도록 주의 바랍니다.`}
197 |
198 |
199 | >
200 |
201 |
202 | );
203 | }
204 |
205 | export default memo(PostWrite, IsEqual);
206 |
--------------------------------------------------------------------------------
/src/screens/postWrite/interfaces.ts:
--------------------------------------------------------------------------------
1 | export interface Thumbnail {
2 | thumbnailUrl?: string;
3 | originalFileName?: string;
4 | }
5 |
6 | export const initThumbnail: Thumbnail = {};
7 |
--------------------------------------------------------------------------------
/src/screens/postWrite/styles.ts:
--------------------------------------------------------------------------------
1 | import { StyleSheet } from 'react-native';
2 | import { defaultTheme } from '@/themes';
3 |
4 | export const styles = StyleSheet.create({
5 | wrapper: {
6 | flex: 1,
7 | backgroundColor: 'white',
8 | },
9 | writeButton: {
10 | fontSize: 18,
11 | color: defaultTheme.primary,
12 | fontFamily: 'PretendardBold',
13 | },
14 | contentContainer: {
15 | marginHorizontal: 20,
16 | },
17 | withBorderRadius: {
18 | borderRadius: 12,
19 | },
20 | thumbnailContainer: {
21 | flexDirection: 'row',
22 | paddingVertical: 24,
23 | },
24 | thumbnail: {
25 | resizeMode: 'cover',
26 | width: 72,
27 | height: 72,
28 | paddingVertical: 24,
29 | },
30 | thumbnailRemoveBadge: {
31 | position: 'absolute',
32 | right: -4,
33 | top: -4,
34 | },
35 | thumbnailCloseIcon: {
36 | width: 14,
37 | height: 14,
38 | },
39 | thumbnailLoader: {
40 | marginLeft: 24,
41 | },
42 | inputContainer: {
43 | paddingTop: 18,
44 | },
45 | caption: {
46 | fontSize: 14,
47 | color: defaultTheme.descDefault,
48 | fontFamily: 'PretendardRegular',
49 | },
50 | });
51 |
--------------------------------------------------------------------------------
/src/screens/settings/index.tsx:
--------------------------------------------------------------------------------
1 | import Constants from 'expo-constants';
2 | import { memo, useCallback } from 'react';
3 | import IsEqual from 'react-fast-compare';
4 | import { SafeAreaView } from 'react-native-safe-area-context';
5 | import { Text, TouchableOpacity, View } from 'react-native-ui-lib';
6 | import { BottomTabNavigationProp } from '@react-navigation/bottom-tabs';
7 | import { useNavigation, CompositeNavigationProp } from '@react-navigation/native';
8 | import { StackNavigationProp } from '@react-navigation/stack';
9 | import CustomHeader from '@/components/customHeader';
10 | import { PermissionedParamsList, ScreenTypes, SettingsParamsList } from '@/configs/screen_types';
11 | import { translate } from '@/i18n';
12 | import { styles } from './styles';
13 |
14 | type ScreenNavigationProps = CompositeNavigationProp<
15 | BottomTabNavigationProp,
16 | StackNavigationProp
17 | >;
18 |
19 | function SettingsScreen(): JSX.Element {
20 | const navigation = useNavigation();
21 |
22 | const onDevelopersPress = useCallback((): void => {
23 | navigation.navigate(ScreenTypes.INFO_DEVELOPERS);
24 | }, []);
25 |
26 | return (
27 |
28 |
29 | {translate('settings.groupInfo')}
30 |
31 | {translate('settings.infoVersion')}
32 | {Constants.manifest?.version}
33 |
34 |
35 | {translate('settings.infoDeveloper')}
36 |
37 |
38 | );
39 | }
40 |
41 | export default memo(SettingsScreen, IsEqual);
42 |
--------------------------------------------------------------------------------
/src/screens/settings/styles.ts:
--------------------------------------------------------------------------------
1 | import { StyleSheet } from 'react-native';
2 | import { defaultTheme } from '@/themes';
3 |
4 | export const styles = StyleSheet.create({
5 | wrapper: {
6 | flex: 1,
7 | backgroundColor: 'white',
8 | },
9 | deafultSpacing: {
10 | paddingHorizontal: 24,
11 | paddingVertical: 8,
12 | },
13 | flexRow: {
14 | flexDirection: 'row',
15 | justifyContent: 'space-between',
16 | },
17 | groupLabel: {
18 | fontSize: 16,
19 | fontFamily: 'PretendardRegular',
20 | color: defaultTheme.descDark,
21 | },
22 | textLabel: {
23 | fontSize: 18,
24 | fontFamily: 'PretendardRegular',
25 | color: defaultTheme.titleDefault,
26 | },
27 | });
28 |
--------------------------------------------------------------------------------
/src/services/posts_service.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import { apiKeys } from '../configs/api_keys';
3 | import { CreatePostRequest, PostCreatedDTO, PostDetailDTO, PostPreviewDTO } from '../dtos/post_dtos';
4 |
5 | const postsInstance = axios.create({
6 | baseURL: apiKeys.postsDomain,
7 | timeout: 5000,
8 | });
9 |
10 | export const createPost = async (data: CreatePostRequest): Promise => {
11 | const res = await postsInstance.post('', data);
12 | return res.data;
13 | };
14 |
15 | export const getPostById = async (id: string): Promise => {
16 | const res = await postsInstance.get(`id/${id}`);
17 | return res.data;
18 | };
19 |
20 | export const getPostsByDistance = async (
21 | currentLatitude: number,
22 | currentLongitude: number,
23 | distanceInKm = 1.0,
24 | pageSize = 10,
25 | pageIndex = 0,
26 | ): Promise => {
27 | const res = await postsInstance.get(
28 | `by/distance/latitude/${currentLatitude}/longitude/${currentLongitude}?distanceInKm=${distanceInKm}&pageSize=${pageSize}&pageIndex=${pageIndex}`,
29 | );
30 | return res.data;
31 | };
32 |
--------------------------------------------------------------------------------
/src/services/upload_service.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import { apiKeys } from '../configs/api_keys';
3 | import { UploadedThumbnailDTO } from '../dtos/upload_dtos';
4 |
5 | const uploadInstance = axios.create({
6 | baseURL: apiKeys.uploadDomain,
7 | timeout: 10000,
8 | });
9 |
10 | export const uploadThumbnail = async (
11 | fileUri: string,
12 | handlePercentage?: (percent: number) => void,
13 | ): Promise => {
14 | const formData = new FormData();
15 | const filename = fileUri.split('/').pop();
16 | const match = /\.(\w+)$/.exec(filename!);
17 | const type = match ? `image/${match[1]}` : `image`;
18 | formData.append('file', {
19 | // @ts-ignore
20 | uri: fileUri,
21 | name: filename,
22 | type,
23 | });
24 |
25 | const res = await fetch(`${apiKeys.uploadDomain}thumbnail`, {
26 | body: formData,
27 | method: 'post',
28 | headers: {
29 | 'Content-Type': 'multipart/form-data',
30 | },
31 | });
32 | if (!res.ok) {
33 | return undefined;
34 | }
35 |
36 | return await res.json();
37 |
38 | // TODO: Change to 'axios' from 'fetch'
39 | /*
40 | const res = await uploadInstance.post('thumbnail', formData, {
41 | headers: {
42 | 'Content-Type': 'multipart/form-data',
43 | // 'Content-Type': `multipart/form-data; boundary=${new Date().getMilliseconds()}`,
44 | },
45 | onUploadProgress: handlePercentage
46 | ? (progressEvent) => {
47 | const percentCompleted = Math.round((progressEvent.loaded * 100) / progressEvent.total);
48 | handlePercentage(percentCompleted);
49 | }
50 | : undefined,
51 | });
52 | return res.data;
53 | */
54 | };
55 |
--------------------------------------------------------------------------------
/src/themes.ts:
--------------------------------------------------------------------------------
1 | export const colors = {
2 | primary: '#6C5CE7',
3 | secondary: '#A29BFE',
4 | black: '#2D3436',
5 | gray900: '#636E72',
6 | gray600: '#888888',
7 | gray500: '#B2BEC3',
8 | gray400: '#DFE6E9',
9 | gray300: '#EDF1F2',
10 | };
11 |
12 | export type Colors = typeof colors;
13 |
14 | export const defaultTheme = {
15 | primary: colors.primary,
16 | secondary: colors.secondary,
17 | black: colors.black,
18 | tabInactive: colors.black,
19 | tabActive: colors.primary,
20 | backgroundDefault: '#F9F9F9',
21 | titleDefault: colors.black,
22 | descDefault: colors.gray500,
23 | descDark: colors.gray900,
24 | captionDefault: colors.gray600,
25 | disabled: colors.gray500,
26 | };
27 |
28 | export const zIndices = {
29 | appbar: 10,
30 | };
31 |
32 | export const APPBAR_HEIGHT = 60;
33 |
--------------------------------------------------------------------------------
/src/types.d.ts:
--------------------------------------------------------------------------------
1 | declare module '*.svg' {
2 | import { SFC } from 'react';
3 | import { SvgProps } from 'react-native-svg';
4 |
5 | const content: SFC;
6 | export default content;
7 | }
8 |
9 | declare module '*.json' {
10 | const content;
11 | export default content;
12 | }
13 |
14 | declare module '*.png' {
15 | const content;
16 | export default content;
17 | }
18 |
--------------------------------------------------------------------------------
/src/utils/recoil_utils.ts:
--------------------------------------------------------------------------------
1 | import { AtomEffect } from 'recoil';
2 | import AsyncStorage from '@react-native-async-storage/async-storage';
3 |
4 | export function persistAtom(key: string, initialValue: T): AtomEffect {
5 | return ({ setSelf, onSet }) => {
6 | setSelf(
7 | AsyncStorage.getItem(key).then((savedValue) => (savedValue != null ? JSON.parse(savedValue) : initialValue)),
8 | );
9 |
10 | // Subscribe to state changes and persist them to localForage
11 | onSet((newValue, _, isReset) => {
12 | if (isReset) {
13 | AsyncStorage.removeItem(key);
14 | return;
15 | }
16 |
17 | AsyncStorage.setItem(key, JSON.stringify(newValue));
18 | });
19 | };
20 | }
21 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "expo/tsconfig.base",
3 | "compilerOptions": {
4 | "strict": true,
5 | "baseUrl": "./",
6 | "rootDir": "./",
7 | "paths": {
8 | "@/*": ["./src/*"],
9 | "@assets/*": ["assets/*"]
10 | },
11 | "esModuleInterop": true,
12 | "allowSyntheticDefaultImports": true,
13 | "experimentalDecorators": true,
14 | "emitDecoratorMetadata": true
15 | },
16 | "include": [
17 | "src/**/*.ts",
18 | "src/**/*.tsx",
19 | "src/types.d.ts"
20 | ],
21 | "exclude": [
22 | "node_modules",
23 | "**/*.test.tsx",
24 | "**/*.spec.ts"
25 | ]
26 | }
27 |
--------------------------------------------------------------------------------