Compare commits

...

37 Commits

Author SHA1 Message Date
Vitalik 2fb7234bd6 Process 4 months ago
Vitalik 23b75add79 fix | calls 4 months ago
Vitalik 05025f0f99 FIX | Group chat screen 5 months ago
Vitalik 603b598cbf CHORE | Fast chat messages loading 5 months ago
Vitalik a8b82da3c0 FEATURE | Multi forward messages 5 months ago
Vitalik 9584ece1ea OPT | Moved some data to states | Chat 5 months ago
Vitalik ef189cc013 FIX | Loading chats to select 5 months ago
Vitalik 5989573193 FIX | Negotiation 6 months ago
Vitalik e2db9778a3 FIX | Call timmers sync 6 months ago
Vitalik 8e18d18897 Merge branch 'call' into stage 6 months ago
Vitalik e957cad230 FIX | android birthday 6 months ago
Vitalik a9b445b1a3 CHORE | Fix tabs buttons in contacts screen 6 months ago
Vitalik 3246c27785 CHORE | Add loader to contacts list screen 6 months ago
Vitalik 349dc65b27 FIX | Fix logout 6 months ago
Vitalik 522bf71a2e FIX | Refresh token concurencyy for socket 6 months ago
Vitalik d454d62523 FEATURE | Fix refresh sessions concurency 6 months ago
Vitalik f405216bd0 Merge branch 'stage' of https://gitlab.work-jetup.site/task-me/application into stage 6 months ago
Yevhen Romanenko b27b8297ea FEATURE | setup sentry enviroments (#45) 6 months ago
Vitalik e2413b8cae Merge branch 'stage' of https://gitlab.work-jetup.site/task-me/application into stage 6 months ago
Yevhen Romanenko 7decce1f3e CHANGE | add sentry config, remove from .gitingnore (#44) 6 months ago
Vitalik 82e9bd074d Merge branch 'stage' of https://gitlab.work-jetup.site/task-me/application into stage 6 months ago
Vitalik abd739a331 FIX | Calss bugs 6 months ago
Yevhen Romanenko 63d119d870 FEATURE | setup application sentry logging (#43) 6 months ago
Vitalik 711ecc5c25 Merge branch 'stage' into call 6 months ago
Vitalik b9265e9995 CHANGE | Reload calls list after end call in real time 6 months ago
Yevhen Romanenko 4a9df62fde BUGFIX | open android media gallery on different AOS versions, update document picker lib version, update msg loader icon (fontello) (#42) 7 months ago
Yevhen Romanenko 0928bb06a3 BUGFIX | Application fixes (#41) 7 months ago
Vitalik e82a9b1d14 FIX | Calls icons&connections 7 months ago
Vitalik 23c32ac791 Merge branch 'stage' into call 7 months ago
Vitalik 4858287b6c fix | calls bugs 7 months ago
Vitalik 471b88e0bf FEATURE | Calls 7 months ago
Vitalik ccefe51229 FEATURE | Calls ios 7 months ago
Vitalik 8777c371eb FEATURE | Call ket 7 months ago
Vitalik b86ec0f991 FEATURE | Voip notifications 7 months ago
Vitalik 4aac8a6006 FEATURE | Calls history 7 months ago
Vitalik 7cfdc600f3 FEATURE | Calls 7 months ago
Vitalik 2ce4f2006b FEATURE | Clls 7 months ago
  1. 6
      .env.development
  2. 4
      .env.production
  3. 4
      .env.stage
  4. 2
      .eslintrc.js
  5. 4
      .gitignore
  6. 6
      android/app/build.gradle
  7. 18
      android/app/src/main/AndroidManifest.xml
  8. BIN
      android/app/src/main/assets/fonts/fontello.ttf
  9. 2
      android/link-assets-manifest.json
  10. 7
      android/sentry.properties
  11. 16
      ios/Podfile.lock
  12. 2
      ios/link-assets-manifest.json
  13. 7
      ios/sentry.properties
  14. 61
      ios/taskme2.xcodeproj/project.pbxproj
  15. 12
      metro.config.js
  16. 23700
      package-lock.json
  17. 3
      package.json
  18. 6
      src/App.tsx
  19. 8
      src/api/calls/requests.ts
  20. 4
      src/api/chat-messages/requests.interfaces.ts
  21. 4
      src/api/chats/requests.interfaces.ts
  22. 23
      src/api/http.service.ts
  23. BIN
      src/assets/fonts/fontello.ttf
  24. 6
      src/config/fontello.json
  25. 2
      src/config/index.ts
  26. 12
      src/config/sentry.config.ts
  27. 75
      src/managers/chat-message.manager.ts
  28. 17
      src/managers/chat.manager.ts
  29. 14
      src/managers/user.manager.ts
  30. 12
      src/modules/account/components/change-date-of-birthday-moda.component.tsx
  31. 2
      src/modules/account/screens/account.screen.tsx
  32. 1
      src/modules/auth/screens/sign-in.screen.tsx
  33. 2
      src/modules/calls/atoms/call-card-info.atom.tsx
  34. 3
      src/modules/calls/core/accept-call.ts
  35. 20
      src/modules/calls/core/stop-call.ts
  36. 3
      src/modules/calls/hooks/use-call-status.hook.ts
  37. 10
      src/modules/calls/hooks/use-call-streams.hook.ts
  38. 3
      src/modules/calls/hooks/use-calls-history.hook.ts
  39. 33
      src/modules/calls/screens/call/index.tsx
  40. 26
      src/modules/calls/services/call.service.ts
  41. 5
      src/modules/calls/services/callkeep-notifications-android.service.ts
  42. 107
      src/modules/calls/services/callkeep/callkeep-android.service.ts
  43. 33
      src/modules/calls/services/callkeep/callkeep-root.service.ts
  44. 118
      src/modules/calls/services/calls-events.service.ts
  45. 24
      src/modules/calls/services/event-handlers/event-handler.ts
  46. 2
      src/modules/calls/services/event-handlers/index.ts
  47. 10
      src/modules/calls/services/event-handlers/negotiation.event-handler.ts
  48. 10
      src/modules/calls/services/ice-status-handler/closed-ice-status.handle.ts
  49. 50
      src/modules/calls/services/ice-status-handler/failed-ice-status.handle.ts
  50. 24
      src/modules/calls/services/ice-status-handler/ice-status-handler-abstract.ts
  51. 33
      src/modules/calls/services/ice-status-handler/ice-status-handler.proxy.ts
  52. 60
      src/modules/calls/services/negotiation.service.ts
  53. 65
      src/modules/calls/services/peer-connection.service.ts
  54. 11
      src/modules/calls/smart-components/calls-list.smart-component.tsx
  55. 57
      src/modules/calls/widgets/incoming-call.widget.tsx
  56. 61
      src/modules/chats/chat/core/actions/chat-message-action-checker.ts
  57. 1
      src/modules/chats/chat/core/actions/index.ts
  58. 81
      src/modules/chats/chat/core/chat-header.ts
  59. 28
      src/modules/chats/chat/core/chat-navigator.ts
  60. 38
      src/modules/chats/chat/core/chat-user-role.ts
  61. 2
      src/modules/chats/chat/core/index.ts
  62. 1
      src/modules/chats/chat/hooks/index.ts
  63. 29
      src/modules/chats/chat/hooks/use-chat-details.hook.ts
  64. 34
      src/modules/chats/chat/states/chat-header.state.ts
  65. 50
      src/modules/chats/chat/states/chat-selected-messages.state.ts
  66. 43
      src/modules/chats/chat/states/chat.state.ts
  67. 4
      src/modules/chats/chat/states/index.ts
  68. 17
      src/modules/chats/chat/states/use-chat-view-mode.state.ts
  69. 39
      src/modules/chats/components/chat-header.component.tsx
  70. 27
      src/modules/chats/helpers/get-chat-info.helper.ts
  71. 16
      src/modules/chats/helpers/get-header-chat-info.helper.ts
  72. 4
      src/modules/chats/helpers/index.ts
  73. 3
      src/modules/chats/hooks/index.ts
  74. 107
      src/modules/chats/hooks/use-chat-details.hook.ts
  75. 288
      src/modules/chats/hooks/use-chat-messages.hook.ts
  76. 14
      src/modules/chats/hooks/use-chat-view-mode-state.hook.ts
  77. 10
      src/modules/chats/hooks/use-chats-list.hook.ts
  78. 13
      src/modules/chats/hooks/use-create-text-message.hook.ts
  79. 17
      src/modules/chats/hooks/use-edit-group-chat.hook.ts
  80. 10
      src/modules/chats/hooks/use-selected-chats.hook.ts
  81. 105
      src/modules/chats/hooks/use-selected-messages.hook.ts
  82. 22
      src/modules/chats/hooks/use-send-files.hook.ts
  83. 90
      src/modules/chats/screens/chat-file-preview.screen.tsx
  84. 52
      src/modules/chats/screens/chat.tsx
  85. 15
      src/modules/chats/screens/chats.screen.tsx
  86. 6
      src/modules/chats/screens/create-conversation.screen.tsx
  87. 6
      src/modules/chats/screens/create-personal.screen.tsx
  88. 60
      src/modules/chats/screens/forward-message.screen.tsx
  89. 70
      src/modules/chats/screens/group-chat-detail.screen.tsx
  90. 25
      src/modules/chats/smart-components/chats-list.smart-component.tsx
  91. 9
      src/modules/chats/transforms/chat-messages.transforms.ts
  92. 6
      src/modules/contacts/screens/contact-detail.screen.tsx
  93. 8
      src/modules/contacts/screens/contacts.screen.tsx
  94. 2
      src/modules/contacts/smart-component/contacts-list.smart-component.tsx
  95. 1
      src/modules/root/hooks/use-app-badge.hook.ts
  96. 2
      src/modules/root/hooks/use-net-connect.hook.ts
  97. 4
      src/modules/root/index.tsx
  98. 3
      src/modules/root/navigation-groups/tab-bar.group.tsx
  99. 5
      src/modules/root/navigation-groups/users.group.tsx
  100. 1
      src/modules/root/states/index.ts
  101. Some files were not shown because too many files have changed in this diff Show More

6
.env.development

@ -1,3 +1,7 @@ @@ -1,3 +1,7 @@
API_URL=http://localhost:3000
SOCKET_URL=http://localhost:3000
ONE_SIGNAL_KEY=8b9066f5-8c3f-49f7-bef4-c5ab621f9d27
ONE_SIGNAL_KEY=8b9066f5-8c3f-49f7-bef4-c5ab621f9d27
SENTRY_ENVIROMENT=develop
SHOW_VERSION_MODAL=true

4
.env.production

@ -2,6 +2,10 @@ API_URL=https://tasks-api.rwsbank.com.ua @@ -2,6 +2,10 @@ API_URL=https://tasks-api.rwsbank.com.ua
SOCKET_URL=https://tasks-api.rwsbank.com.ua
ONE_SIGNAL_KEY=5e1a5e18-33e5-4ed3-8423-45b1abc354c6
SENTRY_ENVIROMENT=production
SHOW_VERSION_MODAL=true
# API_URL=https://taskme-api.work-jetup.site
# SOCKET_URL=https://taskme-api.work-jetup.site
# ONE_SIGNAL_KEY=8b9066f5-8c3f-49f7-bef4-c5ab621f9d27

4
.env.stage

@ -1,3 +1,7 @@ @@ -1,3 +1,7 @@
API_URL=https://taskme-api.work-jetup.site
SOCKET_URL=https://taskme-api.work-jetup.site
ONE_SIGNAL_KEY=8b9066f5-8c3f-49f7-bef4-c5ab621f9d27
SENTRY_ENVIROMENT=stage
SHOW_VERSION_MODAL=false

2
.eslintrc.js

@ -18,6 +18,7 @@ module.exports = { @@ -18,6 +18,7 @@ module.exports = {
],
rules: {
'no-console': 'off',
curly: 'off',
'react-native/no-inline-styles': 0,
'react-native/split-platform-components': 2,
'react-native/no-raw-text': 0,
@ -32,6 +33,7 @@ module.exports = { @@ -32,6 +33,7 @@ module.exports = {
'react-hooks/exhaustive-deps': 0,
'prettier/prettier': 1,
'@typescript-eslint/no-explicit-any': 0,
'react-native/split-platform-components': 0,
'@typescript-eslint/ban-types': [
'off',
{

4
.gitignore vendored

@ -69,9 +69,9 @@ buck-out/ @@ -69,9 +69,9 @@ buck-out/
./vendor
./vendor/*
./vendor/*
vendor
.env
.env

6
android/app/build.gradle

@ -6,7 +6,6 @@ apply from: project(':react-native-config').projectDir.getPath() + "/dotenv.grad @@ -6,7 +6,6 @@ apply from: project(':react-native-config').projectDir.getPath() + "/dotenv.grad
import com.android.build.OutputFile
react {}
def enableSeparateBuildPerCPUArchitecture = false
def enableProguardInReleaseBuilds = false
@ -17,6 +16,7 @@ def reactNativeArchitectures() { @@ -17,6 +16,7 @@ def reactNativeArchitectures() {
return value ? value.split(",") : ["armeabi-v7a", "x86", "x86_64", "arm64-v8a"]
}
apply from: new File(["node", "--print", "require.resolve('@sentry/react-native/package.json')"].execute().text.trim(), "../sentry.gradle")
android {
ndkVersion rootProject.ext.ndkVersion
compileSdkVersion rootProject.ext.compileSdkVersion
@ -26,8 +26,8 @@ android { @@ -26,8 +26,8 @@ android {
applicationId "com.app.task_me"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 230
versionName "2.3"
versionCode 240
versionName "2.6"
resValue "string", "build_config_package", "com.app.task_me"
}

18
android/app/src/main/AndroidManifest.xml

@ -7,6 +7,18 @@ @@ -7,6 +7,18 @@
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO"/>
<uses-permission android:name="android.permission.BIND_TELECOM_CONNECTION_SERVICE"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.CALL_PHONE" />
<uses-permission android:name="android.permission.ANSWER_PHONE_CALLS" />
<uses-permission android:name="android.permission.MANAGE_OWN_CALLS"/>
<uses-permission android:name="android.permission.READ_CALL_LOG" />
<uses-permission android:name="android.permission.WRITE_CALL_LOG" />
<uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-feature android:name="android.hardware.camera" android:required="false" />
<uses-feature android:name="android.hardware.camera.front" android:required="false" />
@ -27,10 +39,8 @@ @@ -27,10 +39,8 @@
<uses-permission android:name="com.oppo.launcher.permission.READ_SETTINGS" tools:node="remove" />
<uses-permission android:name="com.oppo.launcher.permission.WRITE_SETTINGS" tools:node="remove" />
<uses-permission android:name="android.permission.BIND_TELECOM_CONNECTION_SERVICE"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.CALL_PHONE" />
<application
android:name=".MainApplication"

BIN
android/app/src/main/assets/fonts/fontello.ttf

Binary file not shown.

2
android/link-assets-manifest.json

@ -19,7 +19,7 @@ @@ -19,7 +19,7 @@
},
{
"path": "src/assets/fonts/fontello.ttf",
"sha1": "03d635d6ff58b549d8a826a607091e9ebe801af6"
"sha1": "c7687d15e59715fd39d0c1dbfb18e9cf24730b28"
}
]
}

7
android/sentry.properties

@ -0,0 +1,7 @@ @@ -0,0 +1,7 @@
auth.token=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
defaults.org=jetup-digital
defaults.project=application-taskme
defaults.url=https://sentry.io/

16
ios/Podfile.lock

@ -475,7 +475,7 @@ PODS: @@ -475,7 +475,7 @@ PODS:
- react-native-config/Extension (1.5.1)
- react-native-date-picker (4.3.6):
- React-Core
- react-native-document-picker (9.1.1):
- react-native-document-picker (9.1.2):
- React-Core
- react-native-html-to-pdf (0.12.0):
- React-Core
@ -686,6 +686,11 @@ PODS: @@ -686,6 +686,11 @@ PODS:
- RNScreens (3.29.0):
- RCT-Folly (= 2021.07.22.00)
- React-Core
- RNSentry (5.22.2):
- hermes-engine
- React-Core
- React-hermes
- Sentry/HybridSDK (= 8.25.0)
- RNShare (9.4.1):
- React-Core
- RNSoundLevel (1.2.1):
@ -703,6 +708,7 @@ PODS: @@ -703,6 +708,7 @@ PODS:
- SDWebImageWebPCoder (0.8.5):
- libwebp (~> 1.0)
- SDWebImage/Core (~> 5.10)
- Sentry/HybridSDK (8.25.0)
- SocketRocket (0.6.1)
- TOCropViewController (2.7.2)
- Yoga (1.14.0)
@ -795,6 +801,7 @@ DEPENDENCIES: @@ -795,6 +801,7 @@ DEPENDENCIES:
- RNPermissions (from `../node_modules/react-native-permissions`)
- RNReanimated (from `../node_modules/react-native-reanimated`)
- RNScreens (from `../node_modules/react-native-screens`)
- "RNSentry (from `../node_modules/@sentry/react-native`)"
- RNShare (from `../node_modules/react-native-share`)
- "RNSoundLevel (from `../node_modules/@tomlangan/react-native-sound-level`)"
- RNSVG (from `../node_modules/react-native-svg`)
@ -824,6 +831,7 @@ SPEC REPOS: @@ -824,6 +831,7 @@ SPEC REPOS:
- PromisesObjC
- SDWebImage
- SDWebImageWebPCoder
- Sentry
- SocketRocket
- TOCropViewController
@ -986,6 +994,8 @@ EXTERNAL SOURCES: @@ -986,6 +994,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native-reanimated"
RNScreens:
:path: "../node_modules/react-native-screens"
RNSentry:
:path: "../node_modules/@sentry/react-native"
RNShare:
:path: "../node_modules/react-native-share"
RNSoundLevel:
@ -1043,7 +1053,7 @@ SPEC CHECKSUMS: @@ -1043,7 +1053,7 @@ SPEC CHECKSUMS:
react-native-cameraroll: 4701ae7c3dbcd3f5e9e150ca17f250a276154b35
react-native-config: 86038147314e2e6d10ea9972022aa171e6b1d4d8
react-native-date-picker: c28ccf1b3bc3de3bc6a785792a393fee49677f89
react-native-document-picker: 3599b238843369026201d2ef466df53f77ae0452
react-native-document-picker: 86bcfb8e2c94cd587ca9feb0049831b806df590d
react-native-html-to-pdf: 4c5c6e26819fe202971061594058877aa9b25265
react-native-image-picker: 3269f75c251cdcd61ab51b911dd30d6fff8c6169
react-native-netinfo: 48c5f79a84fbc3ba1d28a8b0d04adeda72885fa8
@ -1097,6 +1107,7 @@ SPEC CHECKSUMS: @@ -1097,6 +1107,7 @@ SPEC CHECKSUMS:
RNPermissions: 0a4aa6c2f46c2cd55fbfa095bcdbad20118ea46f
RNReanimated: 43675f1f0e704abe8ebf953fd79b00e66302cda2
RNScreens: 3c5b9f4a9dcde752466854b6109b79c0e205dad3
RNSentry: f6a5aee809d646763640130714d1fdfe69aac36b
RNShare: 32e97adc8d8c97d4a26bcdd3c45516882184f8b6
RNSoundLevel: c031ed233382b6aeabe44b3d5e895b906e69952f
RNSVG: d00c8f91c3cbf6d476451313a18f04d220d4f396
@ -1104,6 +1115,7 @@ SPEC CHECKSUMS: @@ -1104,6 +1115,7 @@ SPEC CHECKSUMS:
RNVoipPushNotification: 543e18f83089134a35e7f1d2eba4c8b1f7776b08
SDWebImage: a7f831e1a65eb5e285e3fb046a23fcfbf08e696d
SDWebImageWebPCoder: 908b83b6adda48effe7667cd2b7f78c897e5111d
Sentry: cd86fc55628f5b7c572cabe66cc8f95a9d2f165a
SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17
TOCropViewController: bf38459e9da6efe414de22087ebd5c530640f4bd
Yoga: d0003f849d2b5224c072cef6568b540d8bb15cd3

2
ios/link-assets-manifest.json

@ -19,7 +19,7 @@ @@ -19,7 +19,7 @@
},
{
"path": "src/assets/fonts/fontello.ttf",
"sha1": "03d635d6ff58b549d8a826a607091e9ebe801af6"
"sha1": "c7687d15e59715fd39d0c1dbfb18e9cf24730b28"
}
]
}

7
ios/sentry.properties

@ -0,0 +1,7 @@ @@ -0,0 +1,7 @@
auth.token=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
defaults.org=jetup-digital
defaults.project=application-taskme
defaults.url=https://sentry.io/

61
ios/taskme2.xcodeproj/project.pbxproj

File diff suppressed because one or more lines are too long

12
metro.config.js

@ -1,11 +1,19 @@ @@ -1,11 +1,19 @@
const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config')
const {
createSentryMetroSerializer
} = require("@sentry/react-native/dist/js/tools/sentryMetroSerializer");
/**
* Metro configuration
* https://facebook.github.io/metro/docs/configuration
*
* @type {import('metro-config').MetroConfig}
*/
const config = {}
const config = {
serializer: {
customSerializer: createSentryMetroSerializer()
}
}
module.exports = mergeConfig(getDefaultConfig(__dirname), config)
module.exports = mergeConfig(getDefaultConfig(__dirname), config)

23700
package-lock.json generated

File diff suppressed because it is too large Load Diff

3
package.json

@ -37,6 +37,7 @@ @@ -37,6 +37,7 @@
"@react-navigation/native": "^6.1.6",
"@react-navigation/native-stack": "^6.9.12",
"@react-navigation/stack": "^6.3.16",
"@sentry/react-native": "^5.22.2",
"@tomlangan/react-native-sound-level": "^1.2.1",
"axios": "^1.4.0",
"buffer": "^6.0.3",
@ -62,7 +63,7 @@ @@ -62,7 +63,7 @@
"react-native-country-flag": "^2.0.2",
"react-native-date-picker": "^4.2.13",
"react-native-device-info": "^10.6.0",
"react-native-document-picker": "^9.0.1",
"react-native-document-picker": "^9.1.2",
"react-native-draggable-switch": "^1.1.1",
"react-native-drawer": "^2.5.1",
"react-native-exception-handler": "^2.10.10",

6
src/App.tsx

@ -18,10 +18,12 @@ import { appEvents } from './shared' @@ -18,10 +18,12 @@ import { appEvents } from './shared'
import Toast from 'react-native-toast-message'
import { toastConfig } from './config/toast.config'
import { VoipNotificationsService } from './services/system'
import { LoggerService, VoipNotificationsService } from './services/system'
import './modules/calls/services/calls-events.service'
import { callKeepService } from './modules/calls/services/callkeep/callkeep'
LoggerService.init()
LogBox.ignoreLogs(['Warning: ...', 'Require cycle: ...'])
LogBox.ignoreAllLogs()
@ -78,4 +80,4 @@ const App: FC = () => { @@ -78,4 +80,4 @@ const App: FC = () => {
)
}
export default App
export default LoggerService.wrap(App)

8
src/api/calls/requests.ts

@ -48,6 +48,14 @@ export const sendNegotiationReq = (callId: string, payload: any) => { @@ -48,6 +48,14 @@ export const sendNegotiationReq = (callId: string, payload: any) => {
return api.post(`calls/negotiation/${callId}`, payload, null, '')
}
export const isNegotiationPossibleReq = (callId: string) => {
return api.get<boolean>(`calls/negotiation-possible/${callId}`, null, '')
}
export const getCallReq = (callId: string) => {
return api.get<ICall>(`calls/${callId}`, {}, '')
}
export const testCallReq = (data: any) => {
return api.post('calls/test', data, null, ' ')
}

4
src/api/chat-messages/requests.interfaces.ts

@ -66,3 +66,7 @@ export interface IForwardMessage { @@ -66,3 +66,7 @@ export interface IForwardMessage {
chatsIds: (number | string)[]
messageId: number
}
export interface IForwardMessageMultyPayload {
chatsIds: (number | string)[]
messagesIds: number[]
}

4
src/api/chats/requests.interfaces.ts

@ -12,7 +12,9 @@ export interface ICreatePersonalChat { @@ -12,7 +12,9 @@ export interface ICreatePersonalChat {
userId: number
offlineKey?: string
}
export interface IFetchChatsParams extends AxiosRequestConfig, IPagination {}
export interface IFetchChatsParams extends AxiosRequestConfig, IPagination {
forceCache?: boolean
}
export interface IFetchChatDetail extends IRequestConfig {
id: number

23
src/api/http.service.ts

@ -8,14 +8,12 @@ import { SetIsForbidden } from '@/store/shared' @@ -8,14 +8,12 @@ import { SetIsForbidden } from '@/store/shared'
import { simpleDispatch } from '@/store/store-helpers'
import { getErrorCode } from '@/shared/helpers'
import { UserFingerprintGenerator } from '@/shared/helpers/fingerprint.helper'
import { refreshSessionService } from '@/services/domain/auth/refresh-session.service'
const store = () => GlobalContainerService.get('store')
export const axiosInstance = axios.create({
baseURL: config.baseUrl,
// headers: {
// 'Content-Type': 'application/json',
// },
timeout: 6000,
})
@ -42,6 +40,16 @@ cachiosInstance.cache = { @@ -42,6 +40,16 @@ cachiosInstance.cache = {
},
}
const refreshSession = async () => {
try {
await refreshSessionService.refresh()
} catch (e) {
authService.forceLogout()
throw e
}
}
// Параметр tryAgainOnError використовується у вийняткових випадках, коли
// не потрібно поновлювати сесію та повторювати запит при помилці на попередній
// спробі зі статусом 401
@ -52,16 +60,15 @@ const request = async <T>( @@ -52,16 +60,15 @@ const request = async <T>(
tryAgainOnError = true,
): Promise<AxiosResponse<T>> => {
try {
let response = await func()
const response = await func()
return response as any as AxiosResponse
} catch (e: any) {
console.log('error network', e)
if (e?.code === 'ECONNABORTED' && retryCount <= 2) {
return request(func, retryCount + 1)
}
if (getErrorCode(e) === 401 && tryAgainOnError && retryCount <= 2) {
await authService.refreshSession()
await refreshSession()
return (await func()) as any as AxiosResponse
}
if (getErrorCode(e) === 403) {
@ -77,7 +84,9 @@ export interface ICacheRequestConfig extends AxiosRequestConfig { @@ -77,7 +84,9 @@ export interface ICacheRequestConfig extends AxiosRequestConfig {
needCache?: true
}
export interface IRequestConfig extends AxiosRequestConfig {}
export interface IRequestConfig extends AxiosRequestConfig {
_?: any
}
interface INonCacheRequestConfig extends AxiosRequestConfig {
needCache?: false

BIN
src/assets/fonts/fontello.ttf

Binary file not shown.

6
src/config/fontello.json

@ -1195,6 +1195,12 @@ @@ -1195,6 +1195,12 @@
"search": [
"videocameraslash"
]
},
{
"uid": "5d2d07f112b8de19f2c0dbfec3e42c05",
"css": "spin5",
"code": 59468,
"src": "fontelico"
}
]
}

2
src/config/index.ts

@ -7,11 +7,13 @@ export const dynamicConfig = { @@ -7,11 +7,13 @@ export const dynamicConfig = {
baseUrl: Config.API_URL,
socketUrl: Config.SOCKET_URL,
oneSignalKey: Config.ONE_SIGNAL_KEY,
showVersionModal: Config.SHOW_VERSION_MODAL === 'true',
appStoreUrl: 'https://apps.apple.com/ua/app/task-me/id1482240685?l=uk',
googlePlayUrl:
'https://play.google.com/store/apps/details?id=com.app.task_me',
}
export const config = {
...dynamicConfig,
fonts,

12
src/config/sentry.config.ts

@ -0,0 +1,12 @@ @@ -0,0 +1,12 @@
import * as Sentry from '@sentry/react-native'
import Config from 'react-native-config'
export const sentryOptions: Sentry.ReactNativeOptions = {
dsn: 'https://681ea9df3c323fbc1d54a9188491a626@o402114.ingest.us.sentry.io/4507231289540608',
environment: Config.SENTRY_ENVIROMENT,
tracesSampleRate: 1.0,
debug: true,
_experiments: {
profilesSampleRate: 1.0,
},
}

75
src/managers/chat-message.manager.ts

@ -33,6 +33,7 @@ import moment from 'moment' @@ -33,6 +33,7 @@ import moment from 'moment'
import { accountManagerInstance } from './account.manager'
import { FakeMessageManager } from './fake-message.manager'
import { FakeTextMessageCreator } from '@/modules/chats/core'
import { LoggerService } from '@/services/system'
export class ChatMessageManager extends Manager {
private CMRepository = ApiChatMessageRepository.getInstance()
@ -71,6 +72,13 @@ export class ChatMessageManager extends Manager { @@ -71,6 +72,13 @@ export class ChatMessageManager extends Manager {
(!param.params.fromId && !param.params.toId),
)
} catch (e) {
LoggerService.logError({
error: e,
level: 'error',
tags: {
section: 'chat-message',
},
})
console.log('SAVE CHAT MESSAGES TO LOCAL DB FAILED', e)
}
}
@ -255,7 +263,16 @@ export class ChatMessageManager extends Manager { @@ -255,7 +263,16 @@ export class ChatMessageManager extends Manager {
uniqueKey: fakeMessage.uniqueKey,
})
} catch (e) {
LoggerService.logError({
error: e,
level: 'error',
tags: {
section: 'chat-message',
},
})
console.log('error on send text message', e)
await this.fakeMessageManager.deleteFakeMessage(
fakeMessage.uniqueKey,
)
@ -285,20 +302,22 @@ export class ChatMessageManager extends Manager { @@ -285,20 +302,22 @@ export class ChatMessageManager extends Manager {
events: null,
}
if (data.mentionsMessage)
if (data.mentionsMessage) {
newMessage.content.mentionsMessage = data.mentionsMessage
}
if (data.replyToId) {
const message = await this.dbCMRepository.getMessage(
data.replyToId.toString(),
)
if (message)
if (message) {
newMessage.content.replyToMessage = _.pick(message, [
'id',
'userId',
'type',
'content',
])
}
}
await this.dbAQRepository.addToQueue({
@ -357,13 +376,14 @@ export class ChatMessageManager extends Manager { @@ -357,13 +376,14 @@ export class ChatMessageManager extends Manager {
const message = await this.dbCMRepository.getMessage(
data.replyToId.toString(),
)
if (message)
if (message) {
newMessage.content.replyToMessage = _.pick(message, [
'id',
'userId',
'type',
'content',
])
}
}
await this.dbAQRepository.addToQueue({
@ -417,6 +437,26 @@ export class ChatMessageManager extends Manager { @@ -417,6 +437,26 @@ export class ChatMessageManager extends Manager {
}
}
public async forwardMessages(
chatsIds: (number | string)[],
messagesIds: number[],
) {
const sortedMessagesIds = messagesIds.sort((a, b) => a - b)
if (this.isConnected === 'online') {
await this.CMRepository.forwardMessageMultiReq({
chatsIds,
messagesIds: sortedMessagesIds,
})
} else {
for await (const id of sortedMessagesIds) {
await this.forwardMessage({
chatsIds,
messageId: id,
})
}
}
}
public async forwardMessage(data: IForwardMessage) {
if (this.isConnected === 'online') {
await this.CMRepository.forwardMessageReq(data)
@ -474,17 +514,26 @@ export class ChatMessageManager extends Manager { @@ -474,17 +514,26 @@ export class ChatMessageManager extends Manager {
}
await this.dbCMRepository.saveMessage(newMessage)
await this.dbChatRepository.updateField(
chatId.toString(),
'lastMessageDate',
newMessage.createdAt,
)
await this.dbChatRepository.updateField(
await this.dbChatRepository.updateFields(
chatId.toString(),
'lastMessage',
newMessage,
{
lastMessageDate: newMessage.createdAt,
lastMessage: newMessage,
},
)
// await this.dbChatRepository.updateField(
// chatId.toString(),
// 'lastMessageDate',
// newMessage.createdAt,
// )
// await this.dbChatRepository.updateField(
// chatId.toString(),
// 'lastMessage',
// newMessage,
// )
offlineKey.push(newMessage.id)
}),
)
@ -755,13 +804,13 @@ export class ChatMessageManager extends Manager { @@ -755,13 +804,13 @@ export class ChatMessageManager extends Manager {
}
private async updateChatLastMessage(chatId?: string | number) {
if (chatId)
if (chatId) {
await this.dbChatRepository.updateField(
chatId.toString(),
'lastMessage',
null,
)
else {
} else {
const ids = await this.dbChatRepository.getAllIds()
await Promise.all(
ids.map(

17
src/managers/chat.manager.ts

@ -27,6 +27,7 @@ import { DbActionsQueueItem } from '@/shared/interfaces/entities' @@ -27,6 +27,7 @@ import { DbActionsQueueItem } from '@/shared/interfaces/entities'
import _ from 'lodash'
import moment from 'moment'
import { accountManagerInstance } from './account.manager'
import { LoggerService } from '@/services/system'
export class ChatManager extends Manager {
private chatRepository = ApiChatRepository.getInstance()
@ -38,6 +39,7 @@ export class ChatManager extends Manager { @@ -38,6 +39,7 @@ export class ChatManager extends Manager {
public async getChats(params: IFetchChatsParams) {
if (this.isConnected === 'online') {
console.log('params', params)
const resp = await this.chatRepository.getChatsListReq(params)
if (resp.data) {
@ -47,6 +49,13 @@ export class ChatManager extends Manager { @@ -47,6 +49,13 @@ export class ChatManager extends Manager {
params.params.page === 1,
)
} catch (e) {
LoggerService.logError({
error: e,
level: 'error',
tags: {
section: 'chats',
},
})
console.log('SAVE CHATS TO LOCAL DB FAILED', e)
}
}
@ -94,11 +103,6 @@ export class ChatManager extends Manager { @@ -94,11 +103,6 @@ export class ChatManager extends Manager {
}),
)
console.log(
'no connection, load chats from local db',
prepared,
count,
)
return {
data: { items: prepared, count },
}
@ -148,7 +152,7 @@ export class ChatManager extends Manager { @@ -148,7 +152,7 @@ export class ChatManager extends Manager {
],
}
if (!chatId)
if (!chatId) {
await this.dbAQRepository.addToQueue({
type: ActionsQueueType.CreatePersonalChat,
data: {
@ -156,6 +160,7 @@ export class ChatManager extends Manager { @@ -156,6 +160,7 @@ export class ChatManager extends Manager {
offlineKey: id,
},
})
}
await this.dbChatRepository.saveChat(dataToSave, null)

14
src/managers/user.manager.ts

@ -1,6 +1,7 @@ @@ -1,6 +1,7 @@
import { IFetchUsersParams } from '@/api/users/requests.interfaces'
import { IFetchUsersResponse } from '@/api/users/responses.interface'
import { ApiUserRepository, DbUserRepository } from '@/repositories'
import { LoggerService } from '@/services/system'
import { Manager } from '@/shared/abstract'
export class UserManager extends Manager {
@ -13,10 +14,21 @@ export class UserManager extends Manager { @@ -13,10 +14,21 @@ export class UserManager extends Manager {
ids ? { ids } : null,
)
if (data) await this.dbUserRepository.saveUsers(data)
if (data) {
await this.dbUserRepository.saveUsers(data)
}
return true
} catch (e) {
LoggerService.logError({
error: e,
level: 'error',
tags: {
section: 'user',
},
})
console.log('ERROR ON LOAD USERS', e)
return false
}
}

12
src/modules/account/components/change-date-of-birthday-moda.component.tsx

@ -25,6 +25,14 @@ export const ChangeDateOfBirthdayModal: FC<IProps> = ({ @@ -25,6 +25,14 @@ export const ChangeDateOfBirthdayModal: FC<IProps> = ({
setNewDate(date)
}, [date])
const onChangeDate = (date: Date) => {
if (date.getTime() > new Date().getTime()) {
setNewDate(new Date())
} else {
setNewDate(date)
}
}
return (
<BottomModal
closeOnPressMask
@ -37,10 +45,10 @@ export const ChangeDateOfBirthdayModal: FC<IProps> = ({ @@ -37,10 +45,10 @@ export const ChangeDateOfBirthdayModal: FC<IProps> = ({
date={newBirthdayDate}
is24hourSource="locale"
androidVariant="nativeAndroid"
onDateChange={setNewDate}
onDateChange={onChangeDate}
locale={'uk'}
mode={'date'}
maximumDate={new Date()}
maximumDate={Platform.OS === 'ios' ? new Date() : undefined}
/>
<Button
title="Застосувати"

2
src/modules/account/screens/account.screen.tsx

@ -31,7 +31,7 @@ import { useSelector } from 'react-redux' @@ -31,7 +31,7 @@ import { useSelector } from 'react-redux'
import { selectMyFactoryName } from '@/store/account'
import { actionsQueueService } from '@/services/domain'
interface IProps extends IRouteParams {}
type IProps = IRouteParams
export const AccountScreen: FC<IProps> = ({ navigation }) => {
const { styles } = useTheme(createStyles)

1
src/modules/auth/screens/sign-in.screen.tsx

@ -13,6 +13,7 @@ import { useAuthorization } from '../hooks' @@ -13,6 +13,7 @@ import { useAuthorization } from '../hooks'
import { PartialTheme } from '@/shared/themes/interfaces'
import { useTheme } from '@/shared/hooks/use-theme.hook'
import { useAndroidPaddingKeyboard } from '@/shared/hooks/use-android-padding-keyboard.hook'
interface IProps extends IRouteParams {}
export const SignInScreen: FC<IProps> = ({ navigation }) => {

2
src/modules/calls/atoms/call-card-info.atom.tsx

@ -47,7 +47,7 @@ export const CallCardInfo: FC<IProps> = ({ @@ -47,7 +47,7 @@ export const CallCardInfo: FC<IProps> = ({
<IconComponent
style={[
styles.icon,
callStatus === CallTypesEnum.OUTGOING && {
callStatus === CallTypesEnum.INCOMING && {
transform: [{ rotate: '180deg' }],
},
]}

3
src/modules/calls/core/accept-call.ts

@ -20,8 +20,6 @@ export class AcceptCall { @@ -20,8 +20,6 @@ export class AcceptCall {
await this.sendAnswer()
this.proccessIceCandidates()
// inCallManager.start({ media: 'video' })
} catch (e) {
console.log(e)
}
@ -29,7 +27,6 @@ export class AcceptCall { @@ -29,7 +27,6 @@ export class AcceptCall {
private prepare() {
this.callDataStore.changeMod(CallMod.Speaking)
// inCallManager.stopRingtone()
}
private setRemoteDescription() {

20
src/modules/calls/core/stop-call.ts

@ -1,7 +1,13 @@ @@ -1,7 +1,13 @@
import { finishCallReq } from '@/api/calls/requests'
import { CallDataStore, ICallStreamsStore } from '../hooks'
import {
CallDataStore,
CallMod,
ICallStreamsStore,
callsStreamsCtr,
} from '../hooks'
import { MediaStream } from 'react-native-webrtc'
import inCallManager from 'react-native-incall-manager'
import { callKeepService } from '../services'
export class StopCall {
constructor(
@ -13,18 +19,14 @@ export class StopCall { @@ -13,18 +19,14 @@ export class StopCall {
return this.getCallDataStore().peerConnection
}
private get callDataStore() {
return this.getCallDataStore()
}
public async stop() {
await finishCallReq({
callId: this.callDataStore.callId,
}).catch(console.log)
this.getCallDataStore().changeMod(CallMod.Finished)
this.peerConnection.close()
this.stopStreams()
callKeepService.stop()
callsStreamsCtr().reset()
try {
inCallManager.stopRingback()
inCallManager.stopRingtone()

3
src/modules/calls/hooks/use-call-status.hook.ts

@ -11,6 +11,9 @@ export const useCallStatus = () => { @@ -11,6 +11,9 @@ export const useCallStatus = () => {
if (['completed', 'connected'].includes(connectedStatus)) {
return null
}
if (['disconnected', 'failed'].includes(connectedStatus)) {
return "Спроба відновити з'єднання"
}
return "З'єднання"
}, [connectedStatus])

10
src/modules/calls/hooks/use-call-streams.hook.ts

@ -21,14 +21,15 @@ export interface ICallStreamsStore { @@ -21,14 +21,15 @@ export interface ICallStreamsStore {
setCameraOn: (isCameraOn: boolean) => void
setRemoveVideoOn: (removeVideoOn: boolean) => void
setSpeakerOn: (isSpeakerOn: boolean) => void
reset: () => void
}
const callsStreamStore = create<ICallStreamsStore>()(set => ({
localStream: null,
remoteStream: null,
isMicOn: true,
isCameraOn: true,
remoteVideoOn: true,
isCameraOn: false,
remoteVideoOn: false,
isSpeakerOn: true,
setLocalStream(localStream) {
@ -53,6 +54,10 @@ const callsStreamStore = create<ICallStreamsStore>()(set => ({ @@ -53,6 +54,10 @@ const callsStreamStore = create<ICallStreamsStore>()(set => ({
setSpeakerOn(isSpeakerOn) {
set({ isSpeakerOn })
},
reset() {
set(callsStreamStore.getInitialState())
},
}))
export const useCallsStream = callsStreamStore
@ -86,7 +91,6 @@ export const useLocalStream = () => { @@ -86,7 +91,6 @@ export const useLocalStream = () => {
function toogleMic(value?: boolean, slientChange = false) {
const isOn = value ? value : !localMicOn
callsStreamsCtr().setMicOn(isOn)
console.log('isOne', isOn)
localStream.getAudioTracks().forEach(track => {
!isOn ? (track.enabled = false) : (track.enabled = true)
})

3
src/modules/calls/hooks/use-calls-history.hook.ts

@ -38,10 +38,11 @@ export const useCallHistory = () => { @@ -38,10 +38,11 @@ export const useCallHistory = () => {
function transformItem(it: ICall, userId: number): ICallListItem {
const targetUser = it.users.find(it => it.id !== userId)
const info = targetUser.info
const date = it.startAt || it.createdAt
return {
callId: it.id,
title: `${info.firstName} ${info.lastName}`,
date: moment(new Date(it.startAt)).format('DD.MM.YY'),
date: moment(new Date(date)).format('DD.MM.YY'),
avatarUrl: info.avatarUrl,
isMissed: it.status !== CallStatus.Finished,
targetUserId: targetUser.id,

33
src/modules/calls/screens/call/index.tsx

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
import React from 'react'
import React, { useEffect, useState } from 'react'
import { CallingAtom, OutgoingcallAtom } from './atoms'
import { Button } from '@/shared'
import {
@ -13,8 +13,9 @@ import { @@ -13,8 +13,9 @@ import {
} from '../../hooks'
import { CallBackground, CallBtn } from '../../components'
import { Dimensions, Modal, StatusBar, StyleSheet, View } from 'react-native'
import { callService } from '../../services'
import { callService, callUtilityService } from '../../services'
import { Timmer } from '@/shared/components/plugins/timmer.component'
import { FailedIceStatusHandler } from '../../services/ice-status-handler/failed-ice-status.handle'
export const CallScreen = () => {
const mod = useCallDataStore(s => s.mod)
@ -25,10 +26,16 @@ export const CallScreen = () => { @@ -25,10 +26,16 @@ export const CallScreen = () => {
const { toogleMic, localMicOn, isCameraOn, toogleCamera } = useLocalStream()
const { isSpeakerOn, toggleSpeaker } = useAudioOutput()
const { isRemoteCameraOn } = useRemoteStream()
const stopCall = () => {
callService.stop()
useCallDataStore.getState().changeMod(CallMod.Finished)
callService.finishCall()
}
useEffect(() => {
FailedIceStatusHandler.clear()
}, [])
const templates = {
[CallMod.Speaking]: (
<CallBackground
@ -47,8 +54,8 @@ export const CallScreen = () => { @@ -47,8 +54,8 @@ export const CallScreen = () => {
}>
<View style={styles.callRow}>
<CallBtn
iconName="speakerhigh"
bgColor={isSpeakerOn ? '#fff' : 'grey'}
iconName={isSpeakerOn ? 'speakerhigh' : 'speakerslash'}
bgColor={isSpeakerOn ? '#fff' : 'rgba(255,255,255,.5)'}
isAnimated={false}
onPress={() => toggleSpeaker()}
iconColor="#000"
@ -59,7 +66,7 @@ export const CallScreen = () => { @@ -59,7 +66,7 @@ export const CallScreen = () => {
iconName={
isCameraOn ? 'videocamera' : 'videocameraslash'
}
bgColor="#fff"
bgColor={isCameraOn ? '#fff' : 'rgba(255,255,255,.5)'}
isAnimated={false}
onPress={() => toogleCamera()}
iconColor="#000"
@ -67,7 +74,7 @@ export const CallScreen = () => { @@ -67,7 +74,7 @@ export const CallScreen = () => {
/>
<CallBtn
iconName={localMicOn ? 'microphone' : 'microphoneslash'}
bgColor="#fff"
bgColor={localMicOn ? '#fff' : 'rgba(255,255,255,.5)'}
isAnimated={false}
onPress={() => toogleMic()}
iconColor="#000"
@ -81,6 +88,16 @@ export const CallScreen = () => { @@ -81,6 +88,16 @@ export const CallScreen = () => {
onPress={stopCall}
style={styles.callBtn}
/>
{/* <CallBtn
iconName="phone-2"
bgColor="red"
isAnimated={false}
onPress={() => {
callUtilityService.peerConnection().restartIce()
}}
style={styles.callBtn}
/> */}
</View>
</CallBackground>
),
@ -98,7 +115,7 @@ export const CallScreen = () => { @@ -98,7 +115,7 @@ export const CallScreen = () => {
needOverlay={false}
showAvatar={false}>
<View style={styles.row}>
<Button title="Вийти" onPress={callService.finishCall} />
<Button title="Вийти" onPress={callService.goOutFromcall} />
</View>
</CallBackground>
),

26
src/modules/calls/services/call.service.ts

@ -5,7 +5,7 @@ import { CallRoot } from './call-root.service' @@ -5,7 +5,7 @@ import { CallRoot } from './call-root.service'
import { peerConnectionService } from './peer-connection.service'
import { RouteKey } from '@/shared'
import { StopCall } from '../core/stop-call'
import { cancelCallReq } from '@/api/calls/requests'
import { cancelCallReq, finishCallReq } from '@/api/calls/requests'
import { RTCSessionDescription } from 'react-native-webrtc'
import { callKeepService } from './callkeep'
import inCallManager from 'react-native-incall-manager'
@ -53,15 +53,27 @@ class CallService extends CallRoot { @@ -53,15 +53,27 @@ class CallService extends CallRoot {
}
}
public async stop() {
new StopCall(useCallDataStore.getState, callsStreamsCtr).stop()
callKeepService.stop()
/**
* When user press stop call button
*/
public async finishCall() {
try {
await finishCallReq({
callId: this.store.callId,
})
new StopCall(useCallDataStore.getState, callsStreamsCtr).stop()
} catch (e) {
console.log('Finish call error', e)
}
}
public async finishCall() {
useCallDataStore.setState(useCallDataStore.getInitialState())
/**
* Called when user press button for finish call
*/
public async goOutFromcall() {
NavigationService.goBack()
callKeepService.stop()
useCallDataStore.setState(useCallDataStore.getInitialState())
}
public async cancel() {

5
src/modules/calls/services/callkeep-notifications-android.service.ts

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
import messaging from '@react-native-firebase/messaging'
import { Alert } from 'react-native'
import RNCallKeep from 'react-native-callkeep'
messaging().setBackgroundMessageHandler(async message => {
@ -28,6 +29,9 @@ messaging().onMessage(async message => { @@ -28,6 +29,9 @@ messaging().onMessage(async message => {
try {
const data = message.data as any
console.log(message)
// Alert.alert('ONMESSAGE', JSON.stringify(message))
if (!data) {
return
}
@ -38,6 +42,7 @@ messaging().onMessage(async message => { @@ -38,6 +42,7 @@ messaging().onMessage(async message => {
}
if (data?.uuid) {
console.log('Should display incoming call')
RNCallKeep.displayIncomingCall(
data.uuid,
data.handle,

107
src/modules/calls/services/callkeep/callkeep-android.service.ts

@ -1,15 +1,110 @@ @@ -1,15 +1,110 @@
import RNCallKeep from 'react-native-callkeep'
import { CallKeepRootService } from './callkeep-root.service'
import { Alert, Linking, PermissionsAndroid } from 'react-native'
import { appEvents } from '@/shared'
export class CallkeepAndroidService extends CallKeepRootService {
protected async onAnswer(data: any) {
RNCallKeep.backToForeground()
RNCallKeep.setConnectionState(data.callUUID, 128)
await delay(100)
// protected async onAnswer(data: any) {
// // this.callAccepted = true
// // RNCallKeep.backToForeground()
// // RNCallKeep.setConnectionState(data.callUUID, 128)
// // await delay(100)
// // RNCallKeep.endAllCalls()
// super.onAnswer(data)
// }
protected async init() {
await this.requestPermissions()
RNCallKeep.setup({
ios: {
appName: 'TaskMe',
includesCallsInRecents: false,
maximumCallGroups: '1',
supportsVideo: false,
},
android: {
alertTitle: 'Permissions required',
alertDescription:
'This application needs to access your phone accounts',
cancelButton: 'Cancel',
okButton: 'ok',
imageName: 'phone_account_icon',
additionalPermissions: [],
foregroundService: {
channelId: 'com.taskme.app',
channelName: 'Foreground service for my app',
notificationTitle: 'My app is running on background',
notificationIcon:
'Path to the resource icon of the notification',
},
selfManaged: true,
},
})
RNCallKeep.endAllCalls()
RNCallKeep.endCall(data.callUUID)
}
private async requestPermissions() {
try {
const granted = await PermissionsAndroid.requestMultiple([
PermissionsAndroid.PERMISSIONS.CAMERA,
PermissionsAndroid.PERMISSIONS.RECORD_AUDIO,
PermissionsAndroid.PERMISSIONS.READ_PHONE_STATE,
PermissionsAndroid.PERMISSIONS.READ_CONTACTS,
// PermissionsAndroid.PERMISSIONS.BLUETOOTH,
// PermissionsAndroid.PERMISSIONS.BLUETOOTH_ADMIN,
PermissionsAndroid.PERMISSIONS.ANSWER_PHONE_CALLS,
PermissionsAndroid.PERMISSIONS.CALL_PHONE,
// PermissionsAndroid.PERMISSIONS.READ_CALL_LOG,
// PermissionsAndroid.PERMISSIONS.WRITE_CALL_LOG,
])
const allPermissionsGranted = Object.values(granted).every(
permission => permission === PermissionsAndroid.RESULTS.GRANTED,
)
if (!allPermissionsGranted) {
Alert.alert(
'Потрібні дозволи',
'Цій програмі потрібен доступ до стану вашого телефону та журналів викликів для належної роботи. Увімкніть необхідні дозволи в налаштуваннях.',
[
{
text: 'Скасувати',
style: 'cancel',
},
{
text: 'Відкрити налаштування',
onPress: () => Linking.openSettings(),
},
],
{ cancelable: false },
)
}
return allPermissionsGranted
} catch (err) {
console.log(err)
}
}
protected async initListeners() {
super.initListeners()
RNCallKeep.addEventListener(
'showIncomingCallUi',
this.onShowIncomingCallUI.bind(this),
)
}
protected async removeListeners() {
super.removeListeners()
}
super.onAnswer(data)
private async onShowIncomingCallUI(data) {
// Alert.alert('Iconming caal from' + data.name)
appEvents.emit('android/showIncomingCallUi', data)
}
}

33
src/modules/calls/services/callkeep/callkeep-root.service.ts

@ -7,12 +7,14 @@ import RNCallKeep from 'react-native-callkeep' @@ -7,12 +7,14 @@ import RNCallKeep from 'react-native-callkeep'
import { CallStatus, appEvents } from '@/shared'
import { DeviceInfoService } from '@/services/system'
import { CallRoot } from '../call-root.service'
import { useLocalStream } from '../../hooks'
import { callService } from '../call.service'
import { callsStreamsCtr, useCallDataStore, useLocalStream } from '../../hooks'
import { StopCall } from '../../core/stop-call'
import { Alert } from 'react-native'
export class CallKeepRootService extends CallRoot {
protected accountInited = false
protected waitingToAnswerData?: any
protected callAccepted: boolean
public register() {
this.init()
@ -38,6 +40,7 @@ export class CallKeepRootService extends CallRoot { @@ -38,6 +40,7 @@ export class CallKeepRootService extends CallRoot {
cancelButton: 'Cancel',
okButton: 'ok',
imageName: 'phone_account_icon',
isHide: true,
additionalPermissions: [],
foregroundService: {
channelId: 'com.company.my',
@ -46,7 +49,7 @@ export class CallKeepRootService extends CallRoot { @@ -46,7 +49,7 @@ export class CallKeepRootService extends CallRoot {
notificationIcon:
'Path to the resource icon of the notification',
},
},
} as any,
})
RNCallKeep.endAllCalls()
@ -89,6 +92,7 @@ export class CallKeepRootService extends CallRoot { @@ -89,6 +92,7 @@ export class CallKeepRootService extends CallRoot {
protected async onAnswer(data: any) {
const { data: call } = await getCallReq(data.callUUID)
if (call.status !== CallStatus.New) {
appEvents.emit('openInfoModal', {
title: 'Дзвінок вже завершився',
@ -117,13 +121,25 @@ export class CallKeepRootService extends CallRoot { @@ -117,13 +121,25 @@ export class CallKeepRootService extends CallRoot {
}
protected async onEnd({ callUUID }) {
if (!this.store.callId) {
Alert.alert('ON END')
if (this.isCallWasRejected()) {
this.callAccepted = null
await rejectCallReq({
callId: callUUID,
})
} else if (this.isCallWasStopedOutside()) {
new StopCall(useCallDataStore.getState, callsStreamsCtr).stop()
}
}
private isCallWasRejected() {
return !this.store.callId && !this.callAccepted
}
private isCallWasStopedOutside() {
return ['completed', 'connected'].includes(this.store.connectedStatus)
}
protected async changeMuted({ muted }) {
console.log('changemuted', muted)
@ -131,8 +147,6 @@ export class CallKeepRootService extends CallRoot { @@ -131,8 +147,6 @@ export class CallKeepRootService extends CallRoot {
}
protected async onCallSettingsUpdateByApp() {
console.log('UPDATED')
const from = this.storeFrom.title
const callId = this.store.callId
@ -151,10 +165,9 @@ export class CallKeepRootService extends CallRoot { @@ -151,10 +165,9 @@ export class CallKeepRootService extends CallRoot {
}
public async handleAcceptedIncomeInOtherDevice(uuid: string) {
RNCallKeep.rejectCall(uuid)
if (this.store.callId) {
RNCallKeep.rejectCall(uuid)
}
RNCallKeep.endAllCalls()
}
}
function delay(ms: number) {
return new Promise(resolve => setTimeout(resolve, ms))
}

118
src/modules/calls/services/calls-events.service.ts

@ -1,48 +1,108 @@ @@ -1,48 +1,108 @@
import { RouteKey, socketEvents } from '@/shared'
import { RouteKey, SocketEvents, socketEvents } from '@/shared'
import { CallRoot } from './call-root.service'
import { CallMod } from '../hooks'
import { CallMod, callsStreamsCtr, useCallDataStore } from '../hooks'
import { callService } from './call.service'
import { DeviceInfoService, NavigationService } from '@/services/system'
import { RTCIceCandidate, RTCSessionDescription } from 'react-native-webrtc'
import { callKeepService } from './callkeep'
import { peerConnectionService } from './peer-connection.service'
import RNCallKeep from 'react-native-callkeep'
import inCallManager from 'react-native-incall-manager'
import { Events } from 'jet-tools'
import { StopCall } from '../core/stop-call'
import { Alert } from 'react-native'
import { NegotiationEventHandler } from './event-handlers/negotiation.event-handler'
class CallsEventsService extends CallRoot {
constructor() {
private readonly socketEvents: Events<SocketEvents>
constructor(socketEvents: Events<any>) {
super()
this.init()
this.socketEvents = socketEvents
this.initIncomeCallEvents()
this.initOutcomeCallEvents()
this.initCommonCallEvents()
}
private init() {
socketEvents.on('call/answered', data => {
console.log('[answered]')
this.store.peerConnection.setRemoteDescription(
new RTCSessionDescription(data.rtcMessage),
)
private get on() {
return this.socketEvents.on.bind(this.socketEvents)
}
peerConnectionService.sendIcecandidates()
this.store.changeMod(CallMod.Speaking)
this.proccessIceCandidates()
})
socketEvents.on('call/new', this.proccessNewCall.bind(this))
socketEvents.on(
'call/ICEcandidate',
this.proccessIceCandidateEvent.bind(this),
)
socketEvents.on('call/negotiation', async data => {
private initCommonCallEvents() {
this.on('call/ICEcandidate', this.proccessIceCandidateEvent.bind(this))
this.on('call/negotiation', async data => {
new NegotiationEventHandler().handle(data)
this.store.peerConnection.setRemoteDescription(data.rtcMessage)
})
socketEvents.on('calls/answered-by-another-device', async data => {
const deviceId = await DeviceInfoService.getDeviceUniqueId()
if (deviceId !== data.deviceUuid) {
RNCallKeep.endAllCalls()
}
})
socketEvents.on('calls/rejected', async () => {
this.on('call/start-timmer', this.handleTimmerStart.bind(this))
this.on('call/end', this.handleEndCallByAnotherPerson.bind(this))
}
private initIncomeCallEvents() {
/**
* Server send data about income call after answer
*/
this.on('call/new', this.proccessNewCall.bind(this))
/**
* Server send these events to all devices that own to target user accepted call,
* so we need to stop income call in other user devices
*/
this.on(
'calls/answered-by-another-device',
this.handleAnsweredByAnotherDevice.bind(this),
)
}
private initOutcomeCallEvents() {
/**
* Server send event if call was accepted
*/
socketEvents.on('call/answered', this.handleAnsweredCall.bind(this))
/**
* Server send event if call was rejected
*/
socketEvents.on('calls/rejected', this.handleRejectedCall.bind(this))
}
private async handleAnsweredCall(data: SocketEvents['call/answered']) {
inCallManager.stopRingback()
this.store.peerConnection.setRemoteDescription(
new RTCSessionDescription(data.rtcMessage),
)
peerConnectionService.sendIcecandidates()
this.store.changeMod(CallMod.Speaking)
this.proccessIceCandidates()
}
private handleRejectedCall() {
setTimeout(() => {
callService.onRejectedByAnotherUser()
})
}, 100)
}
private async handleEndCallByAnotherPerson() {
new StopCall(useCallDataStore.getState, callsStreamsCtr).stop()
}
private async handleAnsweredByAnotherDevice(
data: SocketEvents['calls/answered-by-another-device'],
) {
const deviceId = await DeviceInfoService.getDeviceUniqueId()
if (deviceId === data.deviceUuid) return
RNCallKeep.endAllCalls()
}
private async handleTimmerStart(data: SocketEvents['call/start-timmer']) {
if (this.store.callId !== data.callId) return
this.store.setStartAt(new Date(data.startAt).getTime())
}
private proccessIceCandidates() {
@ -96,4 +156,4 @@ class CallsEventsService extends CallRoot { @@ -96,4 +156,4 @@ class CallsEventsService extends CallRoot {
}
}
export const callsEventsService = new CallsEventsService()
export const callsEventsService = new CallsEventsService(socketEvents)

24
src/modules/calls/services/event-handlers/event-handler.ts

@ -0,0 +1,24 @@ @@ -0,0 +1,24 @@
import {
CallDataStore,
CallFromStore,
ICallStreamsStore,
useCallDataStore,
useCallFromStore,
useCallsStream,
} from '../../hooks'
export abstract class CallEventHandler<T> {
protected get store(): CallDataStore {
return useCallDataStore.getState()
}
protected get storeFrom(): CallFromStore {
return useCallFromStore.getState()
}
protected get streamsStore(): ICallStreamsStore {
return useCallsStream.getState()
}
public abstract handle(data: T): Promise<void>
}

2
src/modules/calls/services/event-handlers/index.ts

@ -0,0 +1,2 @@ @@ -0,0 +1,2 @@
export * from './event-handler'
export * from './negotiation.event-handler'

10
src/modules/calls/services/event-handlers/negotiation.event-handler.ts

@ -0,0 +1,10 @@ @@ -0,0 +1,10 @@
import { SocketEvents } from '@/shared'
import { CallEventHandler } from './event-handler'
export class NegotiationEventHandler extends CallEventHandler<
SocketEvents['call/negotiation']
> {
public async handle(data) {
console.log('data', data)
}
}

10
src/modules/calls/services/ice-status-handler/closed-ice-status.handle.ts

@ -0,0 +1,10 @@ @@ -0,0 +1,10 @@
import { CallMod } from '../../hooks'
import { IceStatusHandler } from './ice-status-handler-abstract'
export class ClosedIceStatusHandler extends IceStatusHandler {
public async handle() {
console.log('HANDLE CLOSED STATUS')
// this.store.changeMod(CallMod.Finished)
}
}

50
src/modules/calls/services/ice-status-handler/failed-ice-status.handle.ts

@ -0,0 +1,50 @@ @@ -0,0 +1,50 @@
import { callService } from '../call.service'
import { IceStatusHandler } from './ice-status-handler-abstract'
/**
* Also use for disconected status, the same actions nedeed
*/
export class FailedIceStatusHandler extends IceStatusHandler {
static database: {
reconnectCount: number
} = {
reconnectCount: 0,
}
private database = FailedIceStatusHandler.database
private maxReconnectCount = 10
public async handle() {
console.log(
'HANDLE FAILED STATUS',
this.maxReconnectCount,
this.database,
)
// callService.finishCall()
if (this.isReachedMaxReconnects()) {
this.clear()
this.reconnectAtemp()
} else {
this.increaseReconnectAttemps()
}
}
private isReachedMaxReconnects() {
return this.database.reconnectCount >= this.maxReconnectCount
}
private increaseReconnectAttemps() {
this.database.reconnectCount++
}
private clear() {
this.database.reconnectCount = 0
}
private reconnectAtemp() {
this.store.peerConnection.restartIce()
}
static clear() {
this.database.reconnectCount = 0
}
}

24
src/modules/calls/services/ice-status-handler/ice-status-handler-abstract.ts

@ -0,0 +1,24 @@ @@ -0,0 +1,24 @@
import {
CallDataStore,
CallFromStore,
ICallStreamsStore,
useCallDataStore,
useCallFromStore,
useCallsStream,
} from '../../hooks'
export abstract class IceStatusHandler {
protected get store(): CallDataStore {
return useCallDataStore.getState()
}
protected get storeFrom(): CallFromStore {
return useCallFromStore.getState()
}
protected get streamsStore(): ICallStreamsStore {
return useCallsStream.getState()
}
abstract handle(): Promise<void>
}

33
src/modules/calls/services/ice-status-handler/ice-status-handler.proxy.ts

@ -0,0 +1,33 @@ @@ -0,0 +1,33 @@
import { CallDataStore } from '../../hooks'
import { ClosedIceStatusHandler } from './closed-ice-status.handle'
import { FailedIceStatusHandler } from './failed-ice-status.handle'
import { IceStatusHandler } from './ice-status-handler-abstract'
export class IceStatusHandlerProxy {
protected iceStatus: RTCIceConnectionState
constructor(protected readonly store: CallDataStore) {}
protected handlers: Partial<
Record<RTCIceConnectionState, { new (): IceStatusHandler }>
> = {
failed: FailedIceStatusHandler,
disconnected: FailedIceStatusHandler,
closed: ClosedIceStatusHandler,
}
public setStatus(iceStatus: RTCIceConnectionState) {
this.iceStatus = iceStatus
this.store.setConnectedStatus(iceStatus)
console.log('ICE STATUS', iceStatus)
return this
}
public handle() {
const Handler = this.handlers[this.iceStatus]
if (Handler) {
new Handler().handle()
}
}
}

60
src/modules/calls/services/negotiation.service.ts

@ -0,0 +1,60 @@ @@ -0,0 +1,60 @@
import {
isNegotiationPossibleReq,
sendNegotiationReq,
} from '@/api/calls/requests'
import { CallRoot } from './call-root.service'
import { callService } from './call.service'
class NegotiationService extends CallRoot {
public async startNegotiation() {
/**
* We don't need negotiation before call initialization
*/
if (this.store.callStartAt < 1) return
const peerConnection = this.store.peerConnection
const isPossible = await this.checkIsNegotiationPossible()
if (!isPossible) {
callService.finishCall()
return
}
console.log('Signaling state', peerConnection.signalingState)
if (peerConnection.signalingState !== 'stable') return
const offer = await peerConnection.createOffer({})
await peerConnection.setLocalDescription(offer)
sendNegotiationReq(this.store.callId, {
type: 'offer',
description: offer,
})
// } else {
// const answer = await peerConnection.createAnswer()
// await peerConnection.setLocalDescription(answer)
// sendNegotiationReq(this.store.callId, {
// type: 'answer',
// description: answer,
// })
// }
}
/**
* Send request to server to check that user is still online
*/
public async checkIsNegotiationPossible(): Promise<boolean> {
try {
const { data } = await isNegotiationPossibleReq(this.store.callId)
return data
} catch (e) {
return false
}
}
}
export const negotiationService = new NegotiationService()

65
src/modules/calls/services/peer-connection.service.ts

@ -1,17 +1,13 @@ @@ -1,17 +1,13 @@
import { MediaStream, RTCPeerConnection } from 'react-native-webrtc'
import { iceServers } from '../configs'
import { CallRoot } from './call-root.service'
import { iceCandidateReq, sendNegotiationReq } from '@/api/calls/requests'
import {
CallMod,
callsStreamsCtr,
initCallsMediaDevices,
useCallDataStore,
} from '../hooks'
import { StopCall } from '../core/stop-call'
import { iceCandidateReq } from '@/api/calls/requests'
import { initCallsMediaDevices } from '../hooks'
import RTCDataChannel from 'react-native-webrtc/lib/typescript/RTCDataChannel'
import { Alert } from 'react-native'
import { appEvents } from '@/shared'
import { IceStatusHandlerProxy } from './ice-status-handler/ice-status-handler.proxy'
import { Alert } from 'react-native'
import { negotiationService } from './negotiation.service'
class PeerConnectionService extends CallRoot {
private icecandidates: any[]
@ -55,61 +51,19 @@ class PeerConnectionService extends CallRoot { @@ -55,61 +51,19 @@ class PeerConnectionService extends CallRoot {
return
}
// iceCandidateReq({
// targetUserId: this.store.targetUserId,
// candidates: [event.candidate],
// })
this.icecandidates.push(event.candidate)
})
peerConnection.addEventListener('iceconnectionstatechange', event => {
this.store.setConnectedStatus(peerConnection.iceConnectionState)
console.log('ICESTATUS', peerConnection.iceConnectionState)
if (
['completed', 'connected'].includes(
peerConnection.iceConnectionState,
)
) {
this.store.setStartAt(new Date().getTime())
}
if (peerConnection.iceConnectionState === 'closed') {
this.store.changeMod(CallMod.Finished)
}
if (
peerConnection.iceConnectionState === 'disconnected' ||
peerConnection.iceConnectionState === 'failed'
) {
new StopCall(useCallDataStore.getState, callsStreamsCtr).stop()
}
new IceStatusHandlerProxy(this.store)
.setStatus(peerConnection.iceConnectionState)
.handle()
})
peerConnection.addEventListener('negotiationneeded', async () => {
try {
console.log('Negotiation needed', peerConnection.signalingState)
if (peerConnection.signalingState === 'stable') {
const offer = await peerConnection.createOffer({})
await peerConnection.setLocalDescription(offer)
console.log('offer')
sendNegotiationReq(this.store.callId, {
type: 'offer',
description: offer,
})
} else {
const answer = await peerConnection.createAnswer()
await peerConnection.setLocalDescription(answer)
console.log('answer')
sendNegotiationReq(this.store.callId, {
type: 'answer',
description: answer,
})
}
negotiationService.startNegotiation()
} catch (error) {
console.error('Error creating offer/answer:', error)
}
@ -154,7 +108,6 @@ class PeerConnectionService extends CallRoot { @@ -154,7 +108,6 @@ class PeerConnectionService extends CallRoot {
}
public async sendIcecandidates() {
console.log('sendice')
await iceCandidateReq({
targetUserId: this.store.targetUserId,
candidates: [...this.icecandidates],

11
src/modules/calls/smart-components/calls-list.smart-component.tsx

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
import { $size, Loading, NotFound, Txt } from '@/shared'
import { $size, Loading, NotFound, Txt, useSocketListener } from '@/shared'
import { SearchForm } from '@/shared/components/forms'
import { useTheme } from '@/shared/hooks/use-theme.hook'
import React, { FC, useCallback, useEffect, useRef, useState } from 'react'
@ -13,6 +13,10 @@ export const CallSmartList: FC = () => { @@ -13,6 +13,10 @@ export const CallSmartList: FC = () => {
const { isLoading, isLoadingNext } = list
const timmer = useRef(null)
useSocketListener('call/end', () => {
list.resetFlatList()
})
useEffect(() => {
if (searchString !== undefined) {
clearTimeout(timmer.current)
@ -53,11 +57,14 @@ export const CallSmartList: FC = () => { @@ -53,11 +57,14 @@ export const CallSmartList: FC = () => {
showsVerticalScrollIndicator={false}
showsHorizontalScrollIndicator={false}
initialNumToRender={10}
onEndReached={list.loadMore}
onEndReached={list.isInit ? list.loadMore : () => null}
keyExtractor={keyExtractor}
refreshing={!searchString && list.isLoading}
onRefresh={list.resetFlatList}
ListFooterComponent={() => {
if (!list.isInit) {
return null
}
if (!isLoading && isLoadingNext) {
return <Loading />
}

57
src/modules/calls/widgets/incoming-call.widget.tsx

@ -1,50 +1,37 @@ @@ -1,50 +1,37 @@
import { RouteKey, useSocketListener, useTheme } from '@/shared'
import { RouteKey, useEventsListener, useTheme } from '@/shared'
import React, { useState } from 'react'
import { Dimensions, Modal, StyleSheet, View } from 'react-native'
import {
callDataStoreHelper,
useCallDataStore,
useCallFromStore,
} from '../hooks'
import { PartialTheme } from '@/shared/themes/interfaces'
import { CallBackground, CallBtn } from '../components'
import { NavigationService } from '@/services/system'
import { RTCIceCandidate } from 'react-native-webrtc'
import { callService } from '../services'
import inCallManager from 'react-native-incall-manager'
import RNCallKeep from 'react-native-callkeep'
export const IncomingCallWidget = () => {
const [isVisible, setVisible] = useState(false)
const { title, avatarImageUrl } = useCallFromStore()
const { styles } = useTheme(createStyles)
const [data, setData] = useState<any>({})
useSocketListener('call/canceled', data => {
setVisible(false)
callService.stop()
})
useSocketListener('call/ICEcandidate', data => {
try {
const candidates = data.candidates
const peerConnection = callDataStoreHelper.peerConnection()
candidates.map(it => {
const item = new RTCIceCandidate(it)
if (peerConnection) {
peerConnection.addIceCandidate(item)
} else {
useCallDataStore.getState().addIcecanidate(item)
}
})
} catch (e) {
console.log(e)
}
})
useEventsListener(
'android/showIncomingCallUi',
data => {
console.log('Show displa incomi n call')
setVisible(true)
setData(data)
inCallManager.startRingtone('_BUNDLE_', [0, 1], null, 1)
},
[],
)
const decline = () => {}
const decline = () => {
RNCallKeep.rejectCall(data.callUUID)
setVisible(false)
setData({})
}
const accept = async () => {
// inCallManager.stopRingtone()
await callService.processAccept()
NavigationService.navigate(RouteKey.Call, {})
setVisible(false)
@ -64,8 +51,8 @@ export const IncomingCallWidget = () => { @@ -64,8 +51,8 @@ export const IncomingCallWidget = () => {
}}>
<View style={styles.container}>
<CallBackground
title={title}
avatarImageUrl={avatarImageUrl}
title={data.name ? data.name : 'Невідомий контакт'}
avatarImageUrl={null}
subtitle="Телефоннє">
<View style={styles.row}>
<CallBtn

61
src/modules/chats/chat/core/actions/chat-message-action-checker.ts

@ -0,0 +1,61 @@ @@ -0,0 +1,61 @@
import { COPY_ENABLED_MESSAGES_TYPES } from '@/modules/chats/consts'
import { netConnectStateCtr } from '@/modules/root/states'
import { MessageType } from '@/shared'
import { IChatMessage } from '@/shared/components/plugins/chat'
import { _ } from 'jet-tools'
import { ChatUserRole } from '../chat-user-role'
export class ChatMessageActionChecker {
constructor(private readonly messages: IChatMessage[]) {}
private get isConnected() {
return netConnectStateCtr().isConnected
}
public isAllOneType(type: MessageType) {
return _.every(this.messages, it => it.type === type)
}
public isOneMessage() {
return this.messages.length === 1
}
public canShare() {
return (
this.isConnected &&
this.isOneMessage() &&
this.messages[0].type !== MessageType.Sticker
)
}
public canCopy() {
if (!this.isConnected) return this.isAllOneType(MessageType.Text)
if (
this.isOneMessage() &&
COPY_ENABLED_MESSAGES_TYPES.includes(this.messages[0].type)
)
return true
if (this.messages.length > 1 && this.isAllOneType(MessageType.Text))
return true
return false
}
public canDeleteForAll() {
if (ChatUserRole.fromAccount().isAdmin()) {
return true
}
return _.every(this.messages, it => {
if (!it.read && it.isMy) return true
return false
})
}
public isSomeOfflineMessages() {
return _.some(this.messages, it => _.isNaN(Number(it.id)))
}
}

1
src/modules/chats/chat/core/actions/index.ts

@ -0,0 +1 @@ @@ -0,0 +1 @@
export * from './chat-message-action-checker'

81
src/modules/chats/chat/core/chat-header.ts

@ -0,0 +1,81 @@ @@ -0,0 +1,81 @@
import { ChatType, IChatDetails, createFullName } from '@/shared'
import { _ } from 'jet-tools'
export class ChatHeader {
constructor(
private readonly chat: IChatDetails,
private readonly accountId: number,
) {}
public getPreparedData() {
return {
...this.getInfo(),
label: this.getLabel(),
}
}
private getInfo() {
if (this.chat.type === ChatType.Personal) return this.getPersonalInfo()
return this.getGroupInfo()
}
private getPersonalInfo() {
const member = _.find(
this.chat.chatMembers,
member => member.userId !== this.accountId,
)
return {
name: createFullName(
member?.user?.firstName,
member?.user?.lastName,
),
previewUrl: member?.user?.avatarUrl,
type: this.chat.type,
}
}
private getGroupInfo() {
return {
name: this.chat.name,
previewUrl: this.chat.previewUrl,
type: this.chat.type,
}
}
private getLabel() {
switch (this.chat.type) {
case ChatType.Group:
return this.getGroupLabel()
case ChatType.Personal:
return this.getPersonalLabel()
default:
return ''
}
}
private getPersonalLabel() {
const member = _.find(
this.chat.chatMembers,
member => member.userId !== this.accountId,
)
if (member?.isOnline) return 'Зараз в мережі'
return 'Не в мережі'
}
private getGroupLabel() {
const membersCount = _.filter(
this.chat.chatMembers,
member => !member.isDeleted,
).length
const onlineCount = this.chat.chatMembers?.reduce(
(count, it) => count + (it.isOnline && !it.isDeleted ? 1 : 0),
0,
)
return `${membersCount} учасників${
onlineCount ? `, ${onlineCount} онлайн` : ''
}`
}
}

28
src/modules/chats/chat/core/chat-navigator.ts

@ -0,0 +1,28 @@ @@ -0,0 +1,28 @@
import { chatManager } from '@/managers'
import { chatStateCtr } from '../states'
import { RouteKey, appEvents } from '@/shared'
class ChatNavigator {
constructor(private navigate?: any) {}
public setNavigate(navigate: any) {
this.navigate = navigate
}
public navigateToChat(chatId: any, unreadMessagesCount?: number) {
chatStateCtr().loadChat(chatId)
this.navigate(RouteKey.Conversation)
if (unreadMessagesCount) {
appEvents.emit('onReadChat', {
chatId: chatId,
unreadCount: unreadMessagesCount,
})
}
chatManager.readChat(chatId)
}
}
export const chatNavigator = new ChatNavigator()

38
src/modules/chats/chat/core/chat-user-role.ts

@ -0,0 +1,38 @@ @@ -0,0 +1,38 @@
import { ChatMemberRole, ChatType, IChatDetails } from '@/shared'
import store from '@/store'
import { selectId } from '@/store/account'
import { chatStateCtr } from '../states'
import { _ } from 'jet-tools'
export class ChatUserRole {
constructor(
private readonly chat: IChatDetails,
private readonly userId: number,
) {}
public get role(): ChatMemberRole {
const member = _.find(
this.chat?.chatMembers,
member => member.userId === this.userId,
)
const role =
member && this.chat?.type === ChatType.Group
? member.role
: ChatMemberRole.Member
return role
}
public isAdmin() {
return this.role === ChatMemberRole.Admin
}
public isMembed() {
return this.role === ChatMemberRole.Member
}
static fromAccount() {
const accountId = selectId(store.getState())
return new ChatUserRole(chatStateCtr().chat, accountId)
}
}

2
src/modules/chats/chat/core/index.ts

@ -0,0 +1,2 @@ @@ -0,0 +1,2 @@
export * from './chat-header'
export * from './chat-user-role'

1
src/modules/chats/chat/hooks/index.ts

@ -0,0 +1 @@ @@ -0,0 +1 @@
export * from './use-chat-details.hook'

29
src/modules/chats/chat/hooks/use-chat-details.hook.ts

@ -0,0 +1,29 @@ @@ -0,0 +1,29 @@
import { useChatState } from '../states'
export const useChatLoader = () => {
const state = useChatState()
async function loadChat(chatId: number, force = false) {
if (state.chatId && !force) {
if (state.chatId === chatId) return
}
await state.loadChat(chatId)
}
return {
chatId: state.chatId,
load: loadChat,
}
}
export const useChatDetailsv2 = () => {
const isLoading = useChatState(s => s.isLoading)
const chat = useChatState(s => s.chat)
return {
isLoading,
chat,
chatId: chat?.id,
}
}

34
src/modules/chats/chat/states/chat-header.state.ts

@ -0,0 +1,34 @@ @@ -0,0 +1,34 @@
import { ChatType, IChatDetails } from '@/shared'
import { IHeaderChatInfo } from '../../interfaces'
import { create } from 'zustand'
import { ChatHeader } from '../core'
import { selectId } from '@/store/account'
import store from '@/store'
const headerChatInfoInitialState: IHeaderChatInfo = {
name: ' ',
previewUrl: null,
type: ChatType.Group,
label: ' ',
}
interface IChatHeaderState {
data: IHeaderChatInfo
set(chat: IChatDetails): void
}
export const chatHeaderState = create<IChatHeaderState>()(set => ({
data: headerChatInfoInitialState,
set(chat) {
const data = new ChatHeader(
chat,
selectId(store.getState()),
).getPreparedData()
set({ data })
},
}))
export const chatHeaderStateCtr = () => chatHeaderState.getState()
export const useChatHeaderState = chatHeaderState

50
src/modules/chats/chat/states/chat-selected-messages.state.ts

@ -0,0 +1,50 @@ @@ -0,0 +1,50 @@
import { IChatMessage } from '@/shared/components/plugins/chat'
import { _ } from 'jet-tools'
import { create } from 'zustand'
interface IChatSelectedMessagesState {
messages: IChatMessage[]
selectMessage: (message: IChatMessage) => void
unselectAll: () => void
}
export const chatSelectedMessagesState = create<IChatSelectedMessagesState>()(
set => ({
messages: [],
selectMessage(message: IChatMessage) {
set(state => {
if (_.find(state.messages, it => it.id === message.id))
return {
messages: state.messages.filter(
it => it.id !== message.id,
),
}
else
return {
messages: [
...state.messages,
_.pick(message, [
'id',
'type',
'content',
'authorId',
'chatId',
'read',
'isMy',
'author',
'isPined',
'createdAt',
]),
],
}
})
},
unselectAll() {
set({ messages: [] })
},
}),
)
export const chatSelectedMessagesStateCtr = () =>
chatSelectedMessagesState.getState()
export const useChatSelectedMessagesState = chatSelectedMessagesState

43
src/modules/chats/chat/states/chat.state.ts

@ -0,0 +1,43 @@ @@ -0,0 +1,43 @@
import { chatManager } from '@/managers'
import { IChatDetails } from '@/shared'
import { create } from 'zustand'
import { chatHeaderStateCtr } from './chat-header.state'
interface IChatState {
chatId: number
chat: IChatDetails
isLoading: boolean
setChat: (chat: IChatDetails) => void
loadChat: (chatId: number) => Promise<void>
clear: () => void
}
export const chatState = create<IChatState>()(set => ({
chatId: null,
isLoading: false,
chat: null,
setChat(chat) {
set({ chat, isLoading: false })
},
async loadChat(chatId) {
set({ isLoading: true, chat: null, chatId: chatId })
const chatDetails = await chatManager.getChatDetail.bind(chatManager)({
id: chatId,
})
chatHeaderStateCtr().set(chatDetails)
set({ chat: chatDetails, isLoading: false })
},
clear() {
set(chatState.getInitialState())
},
}))
export const chatStateCtr = () => chatState.getState()
export const useChatState = chatState

4
src/modules/chats/chat/states/index.ts

@ -0,0 +1,4 @@ @@ -0,0 +1,4 @@
export * from './chat-header.state'
export * from './chat-selected-messages.state'
export * from './chat.state'
export * from './use-chat-view-mode.state'

17
src/modules/chats/chat/states/use-chat-view-mode.state.ts

@ -0,0 +1,17 @@ @@ -0,0 +1,17 @@
import { create } from 'zustand'
import { ChatViewModeEnum } from '../../enums'
interface IChatViewModeState {
mode: ChatViewModeEnum
setMode: (mode: ChatViewModeEnum) => void
}
export const chatViewModeState = create<IChatViewModeState>()(set => ({
mode: ChatViewModeEnum.DEFAULT,
setMode: (mode: ChatViewModeEnum) => {
set({ mode })
},
}))
export const useChatViewModeState = chatViewModeState
export const chatViewModeStateCtr = () => chatViewModeState.getState()

39
src/modules/chats/components/chat-header.component.tsx

@ -1,36 +1,35 @@ @@ -1,36 +1,35 @@
import React, { FC } from 'react'
import React, { FC, useCallback } from 'react'
import { RouteKey, useNav } from '@/shared'
import { Alert } from 'react-native'
import { ChatInfoRowAtom, ChatSelectedMessagesInfoRowAtom } from '../atoms'
import { IHeaderChatInfo } from '../interfaces'
import { simpleDispatch } from '@/store/store-helpers'
import { UnselectChat } from '@/store/chats'
import { ChatViewModeEnum } from '../enums'
import { useChatSelectedMessagesState, useChatViewModeState } from '../hooks'
import {
chatSelectedMessagesStateCtr,
chatViewModeStateCtr,
useChatHeaderState,
useChatSelectedMessagesState,
useChatState,
useChatViewModeState,
} from '../chat/states'
import { useChatSelectedMessages } from '../hooks'
interface IProps {
chatInfo: IHeaderChatInfo
isLoading: boolean
onPressActions: () => void
}
export const ChatHeader = () => {
const { onPressActions } = useChatSelectedMessages()
export const ChatHeader: FC<IProps> = ({
chatInfo,
isLoading,
onPressActions,
}) => {
const nav = useNav()
const chatViewMode = useChatViewModeState(s => s.mode)
const { setMode } = useChatViewModeState()
const isLoading = useChatState(s => s.isLoading)
const chatInfo = useChatHeaderState(s => s.data)
const chatViewMode = useChatViewModeState(s => s.mode)
const selectedMessages = useChatSelectedMessagesState(s => s.messages)
const { unselectAll: unselectAllMessages } = useChatSelectedMessagesState()
const cancelSelectMode = () => {
unselectAllMessages()
setMode(ChatViewModeEnum.DEFAULT)
}
const cancelSelectMode = useCallback(() => {
chatSelectedMessagesStateCtr().unselectAll()
chatViewModeStateCtr().setMode(ChatViewModeEnum.DEFAULT)
}, [])
const goBack = () => {
nav.goBack()

27
src/modules/chats/helpers/get-chat-info.helper.ts

@ -1,27 +0,0 @@ @@ -1,27 +0,0 @@
import { createFullName, IChatDetails } from '@/shared'
import _ from 'lodash'
import { IHeaderChatInfo } from '../interfaces'
export const getGroupChatInfo = (
chatDetail: IChatDetails,
): Omit<IHeaderChatInfo, 'label'> => ({
name: chatDetail.name,
previewUrl: chatDetail.previewUrl,
type: chatDetail.type,
})
export const getPersonalChatInfo = (
chatDetail: IChatDetails,
accountId: number,
): Omit<IHeaderChatInfo, 'label'> => {
const member = _.find(
chatDetail.chatMembers,
member => member.userId !== accountId,
)
return {
name: createFullName(member?.user?.firstName, member?.user?.lastName),
previewUrl: member?.user?.avatarUrl,
type: chatDetail.type,
}
}

16
src/modules/chats/helpers/get-header-chat-info.helper.ts

@ -1,16 +0,0 @@ @@ -1,16 +0,0 @@
import { IChatMember } from "@/shared"
import _ from "lodash"
export const getHeaderGroupChatInfo = (membersCount: number, onlineCount: number) => {
return `${membersCount} учасників${onlineCount ? `, ${onlineCount} онлайн` : ''}`
}
export const getHeaderPersonalChatInfo = (members: IChatMember[], accountId: number) => {
const member = _.find(
members,
member => member.userId !== accountId,
)
if (member?.isOnline) return 'Зараз в мережі'
return 'Не в мережі'
}

4
src/modules/chats/helpers/index.ts

@ -1,6 +1,4 @@ @@ -1,6 +1,4 @@
export * from './chat-messages.helper'
export * from './chats-helpers.helper'
export * from './get-chat-info.helper'
export * from './get-header-chat-info.helper'
export * from './get-time-from-message-send.helper'
export * from './get-copied-messages-content.helper'
export * from './get-time-from-message-send.helper'

3
src/modules/chats/hooks/index.ts

@ -7,7 +7,6 @@ export * from './use-create-text-message.hook' @@ -7,7 +7,6 @@ export * from './use-create-text-message.hook'
export * from './use-edit-group-chat.hook'
export * from './use-pined-messages.hook'
export * from './use-selected-chats.hook'
export * from './use-selected-messages.hook'
export * from './use-send-files.hook'
export * from './use-send-sticker.hook'
export * from './use-selected-messages.hook'
export * from './use-chat-view-mode-state.hook'

107
src/modules/chats/hooks/use-chat-details.hook.ts

@ -1,82 +1,25 @@ @@ -1,82 +1,25 @@
import { chatManager } from '@/managers'
import {
AppEvents,
ChatType,
IChatDetails,
SocketEvents,
useEventsListener,
useSocketListener,
} from '@/shared'
import { selectId } from '@/store/account'
import _ from 'lodash'
import { useState, useEffect } from 'react'
import { useSelector } from 'react-redux'
import { getHeaderGroupChatInfo, getHeaderPersonalChatInfo } from '../helpers'
import { IHeaderChatInfo } from '../interfaces'
import { transformChatDetailToHeaderChatInfo } from '../transforms'
const headerChatInfoInitialState: IHeaderChatInfo = {
name: ' ',
previewUrl: null,
type: ChatType.Group,
label: ' ',
}
export const useChatDetails = (chatId: number | string) => {
const accountId = useSelector(selectId)
const [chatDetails, setChat] = useState<IChatDetails>(null)
const [isLoading, setLoading] = useState<boolean>(false)
import { useChatDetailsv2 } from '../chat/hooks'
import { chatStateCtr } from '../chat/states'
const [headerChatInfo, setHeaderInfo] = useState<IHeaderChatInfo>(
headerChatInfoInitialState,
)
export const useChatDetails = () => {
const { isLoading, chat, chatId } = useChatDetailsv2()
const fetchDetails = async () => {
setLoading(true)
// const chatDetails = await chatsService.fetchDetails({id: Number(chatId)})
const chatDetails = await chatManager.getChatDetail.bind(chatManager)({
id: chatId,
})
setHeaderChatInfo(chatDetails)
setChat(chatDetails)
setLoading(false)
}
const setHeaderChatInfo = (chatDetails: IChatDetails) => {
const headerChatInfo = transformChatDetailToHeaderChatInfo(
chatDetails,
accountId,
)
const onlineUsersCount = chatDetails.chatMembers?.reduce(
(count, it) => count + (it.isOnline && !it.isDeleted ? 1 : 0),
0,
)
const headerLabel = {
[ChatType.Group]: getHeaderGroupChatInfo(
_.filter(chatDetails.chatMembers, member => !member.isDeleted)
.length,
onlineUsersCount,
),
[ChatType.Personal]: getHeaderPersonalChatInfo(
chatDetails.chatMembers,
accountId,
),
}
setHeaderInfo({
...headerChatInfo,
label: headerLabel[chatDetails.type],
})
chatStateCtr().loadChat(Number(chatId))
}
const reload = () => fetchDetails()
useEffect(() => {
if (chatId) fetchDetails()
}, [chatId])
// useEffect(() => {
// if (chatId) fetchDetails()
// }, [chatId])
const updateChatData = (id: number | string) => {
if (id.toString() !== chatId.toString()) return
@ -104,12 +47,8 @@ export const useChatDetails = (chatId: number | string) => { @@ -104,12 +47,8 @@ export const useChatDetails = (chatId: number | string) => {
}
const reloadIfUserIsChatMember = (userId: number, isOnline: boolean) => {
const member = _.find(
chatDetails?.chatMembers,
it => it.userId === userId,
)
if (member && member.isOnline !== isOnline)
updateChatData(chatDetails?.id)
const member = _.find(chat?.chatMembers, it => it.userId === userId)
if (member && member.isOnline !== isOnline) updateChatData(chat?.id)
}
const onUserConnected = (data: { userId: number }) => {
@ -122,42 +61,42 @@ export const useChatDetails = (chatId: number | string) => { @@ -122,42 +61,42 @@ export const useChatDetails = (chatId: number | string) => {
const onMessageDeleted = (data: SocketEvents['chat/delete-message']) => {
const pinnedIsDeleted = _.find(
chatDetails.pinedMessages,
chat.pinedMessages,
it => it.id.toString() === data.messageId.toString(),
)
if (pinnedIsDeleted) updateChatData(data.chatId)
}
useSocketListener('chat/new-members', onNewMembers, [chatDetails])
useSocketListener('chat/new-members', onNewMembers, [chat])
useSocketListener('chat/delete-member', onDeleteMember, [chatDetails])
useSocketListener('chat/delete-member', onDeleteMember, [chat])
useSocketListener('chat/edit-chat', onEditChat, [chatDetails])
useSocketListener('chat/edit-chat', onEditChat, [chat])
useSocketListener('chat/change-role', onChangeRoleInChat, [chatDetails])
useSocketListener('chat/change-role', onChangeRoleInChat, [chat])
useSocketListener('chat/pined-message', onPinedMessage, [chatDetails])
useSocketListener('chat/pined-message', onPinedMessage, [chat])
useSocketListener('user/connected', onUserConnected, [chatDetails])
useSocketListener('user/connected', onUserConnected, [chat])
useSocketListener('user/disconnected', onUserDisconnected, [chatDetails])
useSocketListener('user/disconnected', onUserDisconnected, [chat])
useSocketListener('chat/delete-message', onMessageDeleted, [chatDetails])
useSocketListener('chat/delete-message', onMessageDeleted, [chat])
// Для оновлення даних чату при закріпленні повідомлення чату в режимі офлайн
useEventsListener('onPinnedMessage', onPinedMessage, [chatDetails])
useEventsListener('onPinnedMessage', onPinedMessage, [chat])
useEventsListener(
'reloadChatDetail',
(data: AppEvents['reloadChatDetail']) => updateChatData(data.chatId),
[chatDetails],
[chat],
)
return {
chatDetails,
headerChatInfo,
chat: chat,
isLoading,
reload,
chatId,
}
}

288
src/modules/chats/hooks/use-chat-messages.hook.ts

@ -10,12 +10,13 @@ import { @@ -10,12 +10,13 @@ import {
IChatMessage,
MessageType,
RouteKey,
useCacheState,
useEventsListener,
useNav,
useSocket,
useSocketListener,
} from '@/shared'
import { useIdsList } from '@/shared/hooks/use-ids-list.hook'
import { LoadingDirection, useIdsList } from '@/shared/hooks/use-ids-list.hook'
import { selectAccount, selectId } from '@/store/account'
import { UnselectChat } from '@/store/chats'
import { simpleDispatch } from '@/store/store-helpers'
@ -30,20 +31,33 @@ import { chatMessageManager } from '@/managers' @@ -30,20 +31,33 @@ import { chatMessageManager } from '@/managers'
import { isMessageEqualURL } from '@/shared/helpers'
import { COPY_ENABLED_MESSAGES_TYPES } from '../consts'
import { fakeMessageManager } from '@/managers/fake-message.manager'
import { useChatState } from '../chat/states'
import { chatViewModeStateCtr } from '../chat/states/use-chat-view-mode.state'
import { transformMessages } from '../transforms'
export const useChatMessages = () => {
const chat = useChatState(s => s.chat)
const chatId = useChatState(s => s.chatId)
const firstMessageId = chat?.firstMessageId
const lastMessageId = chat?.lastMessageId
const chatMembers = chat?.chatMembers
export const useChatMessages = (
chatId: number | string,
firstMessageId: number | string,
lastMessageId: number | string,
chatMembers: IChatMember[],
setChatViewMode: (mode: ChatViewModeEnum) => void,
) => {
const accountId = useSelector(selectId)
const socket = useSocket()
const nav = useNav()
const [replyTo, setReplyTo] = useState<IChatMessage>(null)
const [scrollToId, setScrollToId] = useState(null)
const [transformedMessages, setTransformedMessages] = useCacheState<any[]>({
cacheKey: chatId ? `chat-messages/${chatId}` : undefined,
initData: [],
cacheDataValidFunc(data) {
return !!data && Array.isArray(data)
},
prepareForCache(data) {
return Array.isArray(data) && data.slice(-30)
},
})
const {
items: messages,
@ -65,16 +79,25 @@ export const useChatMessages = ( @@ -65,16 +79,25 @@ export const useChatMessages = (
req: chatMessageManager.getChatMessages.bind(chatMessageManager),
loadParams: {
sortField: 'id',
// chatId: chatId?.toString(),
},
needInit: false,
clearWhenReload: false,
lastMessageId,
firstMessageId,
initStatus: LoadingDirection.none,
})
useEffect(() => {
if (chatId) loadItems({ chatId }, true)
if (chatMembers && messages)
setTransformedMessages(
transformMessages(messages, chatMembers, accountId),
)
}, [messages, chatMembers])
useEffect(() => {
if (chatId) {
loadItems({ chatId }, true)
}
}, [chatId])
const onCopy = useCallback((message: IChatMessage) => {
@ -109,10 +132,14 @@ export const useChatMessages = ( @@ -109,10 +132,14 @@ export const useChatMessages = (
const checkCanDeleteForAll = useCallback(
(message: IChatMessage, role: ChatMemberRole) => {
if (role === ChatMemberRole.Admin) return true
if (role === ChatMemberRole.Admin) {
return true
}
const isAuthor = message.userId === accountId
if (_.isEmpty(message.events) && isAuthor) return true
if (_.isEmpty(message.events) && isAuthor) {
return true
}
const viewed = _.some(message.events, event => {
const keys = Object.keys(event)
@ -159,11 +186,17 @@ export const useChatMessages = ( @@ -159,11 +186,17 @@ export const useChatMessages = (
)
}, [])
const getAuthorName = (userId: number, members: IChatMember[]) => {
const member = _.find(members, member => member.userId === userId)
const getAuthorName = useCallback(
(userId: number, members: IChatMember[]) => {
const member = _.find(members, member => member.userId === userId)
return createFullName(member?.user?.firstName, member?.user?.lastName)
}
return createFullName(
member?.user?.firstName,
member?.user?.lastName,
)
},
[],
)
const onShare = async (message: IChatMessage) => {
const title = getAuthorName(message?.userId, chatMembers)
@ -171,7 +204,9 @@ export const useChatMessages = ( @@ -171,7 +204,9 @@ export const useChatMessages = (
if (message.type === MessageType.Text) {
if (!isMessageEqualURL(message.content?.message)) {
fsService.shareText(title, message.content?.message)
} else fsService.shareUrl(title, message.content?.message)
} else {
fsService.shareUrl(title, message.content?.message)
}
}
if (message.type === MessageType.Image) {
@ -213,11 +248,12 @@ export const useChatMessages = ( @@ -213,11 +248,12 @@ export const useChatMessages = (
title,
message.content?.originalMessage?.content?.message,
)
} else
} else {
fsService.shareUrl(
title,
message.content?.originalMessage?.content?.message,
)
}
} else {
fsService.shareFileOutside(
message.content?.originalMessage?.content?.fileUrl,
@ -227,9 +263,9 @@ export const useChatMessages = ( @@ -227,9 +263,9 @@ export const useChatMessages = (
}
}
const onEdit = (message: IChatMessage) => {
const onEdit = useCallback((message: IChatMessage) => {
appEvents.emit('editMessage', { message })
}
}, [])
const onCancel = useCallback(
async (message: IChatMessage) =>
@ -237,7 +273,9 @@ export const useChatMessages = ( @@ -237,7 +273,9 @@ export const useChatMessages = (
[],
)
const onSelect = () => setChatViewMode(ChatViewModeEnum.SELECT)
const onSelect = useCallback(() => {
chatViewModeStateCtr().setMode(ChatViewModeEnum.SELECT)
}, [])
const actions = {
[ChatMessageActionEnum.COPY]: onCopy,
@ -293,8 +331,11 @@ export const useChatMessages = ( @@ -293,8 +331,11 @@ export const useChatMessages = (
}
const onPressMessagePreview = async (id: number) => {
if (_.find(messages, message => message.id === id)) setScrollToId(id)
else loadMessage(id)
if (_.find(messages, message => message.id === id)) {
setScrollToId(id)
} else {
loadMessage(id)
}
}
const onLoadNew = async (limit?: number) => {
@ -303,7 +344,7 @@ export const useChatMessages = ( @@ -303,7 +344,7 @@ export const useChatMessages = (
// ****** APP EVENTS AND SOCKET SIGNALS HANDLERS *****
const messagesContainsItem = (itemId: number | string) =>
const isMessagesContainsItem = (itemId: number | string) =>
_.find(messages, message => message.id.toString() === itemId.toString())
const updateEvents = (
@ -318,7 +359,9 @@ export const useChatMessages = ( @@ -318,7 +359,9 @@ export const useChatMessages = (
_.includes(Object.keys(it), userId.toString()) &&
it[userId] === event,
)
if (userEvent) return item
if (userEvent) {
return item
}
return {
...item,
@ -335,118 +378,131 @@ export const useChatMessages = ( @@ -335,118 +378,131 @@ export const useChatMessages = (
chatId as number,
)
if (!_.isNaN(Number(message.id)))
socket.emit('chat/read-message', {
userId: accountId,
messagesIds: [message.id],
chatId,
})
if (message.userId === accountId) setScrollToId(message.id)
if (message.userId === accountId) {
setScrollToId(message.id)
}
return updatedMessages
}
const isMessageFitCurrentChat = useCallback(
(message: IChatMessage, chatId) => {
return String(message?.chatId) === String(chatId)
},
[],
)
const onNewOneMessage = async (
message: IChatMessage,
offlineKey?: string,
) => {
if (
message?.chatId?.toString() === chatId.toString() &&
!messagesContainsItem(message?.id)
!isMessageFitCurrentChat(message, chatId) ||
isMessagesContainsItem(message?.id)
) {
if (message.uniqueKey) {
const updatedMessages = await processNewMessageWithUniqueKey(
message,
)
return
}
if (message.uniqueKey) {
const updatedMessages = await processNewMessageWithUniqueKey(
message,
)
return _setItems([
...updatedMessages,
message as Omit<IChatMessage, 'id'> & { id: number },
])
}
return _setItems([
...updatedMessages,
message as Omit<IChatMessage, 'id'> & { id: number },
])
}
if (offlineKey && messagesContainsItem(offlineKey))
_setItems(
messages.map(it => {
if (it.id.toString() === offlineKey)
return {
...it,
id: message.id,
} as Omit<IChatMessage, 'id'> & {
id: number
}
return it
}),
)
else
_setItems([
message as Omit<IChatMessage, 'id'> & { id: number },
...messages,
])
chatMessageManager.onNewMessage(message)
if (!_.isNaN(Number(message.id)))
socket.emit('chat/read-message', {
userId: accountId,
messagesIds: [message.id],
chatId,
})
if (message.userId === accountId && !offlineKey)
setScrollToId(message.id)
if (offlineKey && isMessagesContainsItem(offlineKey)) {
_setItems(
messages.map(it => {
if (it.id.toString() === offlineKey) {
return {
...it,
id: message.id,
} as Omit<IChatMessage, 'id'> & {
id: number
}
}
return it
}),
)
} else {
_setItems([
message as Omit<IChatMessage, 'id'> & { id: number },
...messages,
])
}
chatMessageManager.onNewMessage(message)
if (!_.isNaN(Number(message.id))) {
socket.emit('chat/read-message', {
userId: accountId,
messagesIds: [message.id],
chatId,
})
}
if (message.userId === accountId && !offlineKey) {
setScrollToId(message.id)
}
}
const onNewMessages = (message: IChatMessage[], offlineKey?: string[]) => {
if (_.isEmpty(message) || Number(message[0].chatId) !== chatId) return
if (_.isEmpty(message) || Number(message[0].chatId) !== chatId) {
return
}
const messagesToAdd = _.filter(
message,
it => !messagesContainsItem(it.id),
it => !isMessagesContainsItem(it.id),
) as (Omit<IChatMessage, 'id'> & { id: number })[]
let restMessages = [...messages]
if (offlineKey)
if (offlineKey) {
restMessages = messages.filter(
it => !_.includes(offlineKey, it.id.toString()),
)
}
if (!_.isEmpty(messagesToAdd)) {
_setItems([...messagesToAdd, ...restMessages])
if (_.isEmpty(messagesToAdd)) {
return
}
chatMessageManager.onNewMessage(message)
_setItems([...messagesToAdd, ...restMessages])
const messagesIds = messagesToAdd.map(it => it.id)
const onlineMessagesIds = _.filter(
messagesIds,
it => !_.isNaN(Number(it)),
)
chatMessageManager.onNewMessage(message)
socket.emit('chat/read-message', {
userId: accountId,
messagesIds: onlineMessagesIds,
chatId,
})
}
const messagesIds = messagesToAdd.map(it => it.id)
const onlineMessagesIds = _.filter(
messagesIds,
it => !_.isNaN(Number(it)),
)
socket.emit('chat/read-message', {
userId: accountId,
messagesIds: onlineMessagesIds,
chatId,
})
}
const onNewMessage = (data: {
message: IChatMessage | IChatMessage[]
offlineKey?: string | string[]
}) => {
if (_.isArray(data.message))
if (_.isArray(data.message)) {
onNewMessages(
data.message as IChatMessage[],
data.offlineKey as string[],
)
else
} else {
onNewOneMessage(
data.message as IChatMessage,
data.offlineKey as string,
)
}
}
const onMessageDeleted = (
@ -454,16 +510,18 @@ export const useChatMessages = ( @@ -454,16 +510,18 @@ export const useChatMessages = (
messageId: number | string
chatId: number | string
},
updateLocal: boolean = true,
updateLocal = true,
) => {
if (data?.chatId?.toString() !== chatId.toString()) return
if (data?.chatId?.toString() !== chatId.toString()) {
return
}
const updatedMessages = messages.map(it => {
if (
it.content?.replyToMessage &&
it.content?.replyToMessage?.id?.toString() ===
data.messageId.toString()
)
) {
return {
...it,
content: {
@ -474,6 +532,7 @@ export const useChatMessages = ( @@ -474,6 +532,7 @@ export const useChatMessages = (
},
},
}
}
return it
})
@ -490,19 +549,23 @@ export const useChatMessages = ( @@ -490,19 +549,23 @@ export const useChatMessages = (
}
const onClearChat = (data: { chatId: number }) => {
if (data?.chatId !== chatId) return
if (data?.chatId !== chatId) {
return
}
_setItems([])
}
const onDeleteChat = (data: { chatId: number }) => {
if (data?.chatId !== chatId) return
if (data?.chatId !== chatId) {
return
}
simpleDispatch(new UnselectChat())
nav.navigate(RouteKey.Chats)
}
const onPinedMessage = (
data: { chatId: number; messageId: number },
updateLocal: boolean = true,
updateLocal = true,
) => {
if (
data?.chatId.toString() !== chatId.toString() ||
@ -510,26 +573,31 @@ export const useChatMessages = ( @@ -510,26 +573,31 @@ export const useChatMessages = (
messages,
message => message.id.toString() === data.messageId.toString(),
)
)
) {
return
}
const newItems = messages.map(it => {
if (it.id.toString() === data.messageId.toString())
if (it.id.toString() === data.messageId.toString()) {
return {
...it,
isPined: !it.isPined,
}
}
return it
})
_setItems(newItems)
if (updateLocal) chatMessageManager.onMessagePinned(data.messageId)
if (updateLocal) {
chatMessageManager.onMessagePinned(data.messageId)
}
}
const onChatIsRead = (data: { chatId: number; userId: number }) => {
if (Number(data?.chatId) !== chatId || data?.userId === accountId)
if (Number(data?.chatId) !== chatId || data?.userId === accountId) {
return
}
const newItems = updateEvents(
messages,
@ -539,7 +607,7 @@ export const useChatMessages = ( @@ -539,7 +607,7 @@ export const useChatMessages = (
_setItems(newItems as (Omit<IChatMessage, 'id'> & { id: number })[])
}
const onUpdateMessage = ({ message }, updateLocal: boolean = true) => {
const onUpdateMessage = ({ message }, updateLocal = true) => {
const newItems = messages.map(it => {
if (it.id.toString() === message.id.toString()) {
return message
@ -549,8 +617,9 @@ export const useChatMessages = ( @@ -549,8 +617,9 @@ export const useChatMessages = (
_setItems(newItems)
if (message.chatId.toString() === chatId?.toString() && updateLocal)
if (message.chatId.toString() === chatId?.toString() && updateLocal) {
chatMessageManager.onUpdateMessage(message)
}
}
// ****** APP EVENTS AND SOCKET SIGNALS LISTENERS *****
@ -588,7 +657,9 @@ export const useChatMessages = ( @@ -588,7 +657,9 @@ export const useChatMessages = (
useEventsListener(
'onNeedReloadMessages',
() => {
if (chatId) resetList()
if (chatId) {
resetList()
}
},
[chatId],
)
@ -596,7 +667,9 @@ export const useChatMessages = ( @@ -596,7 +667,9 @@ export const useChatMessages = (
useEventsListener(
'onAppFocused',
() => {
if (chatId) resetList()
if (chatId) {
resetList()
}
},
[chatId],
)
@ -639,6 +712,7 @@ export const useChatMessages = ( @@ -639,6 +712,7 @@ export const useChatMessages = (
return {
messages,
transformedMessages,
loadMore: onLoadNew,
loadEarlier: loadOld,
loadMessage,

14
src/modules/chats/hooks/use-chat-view-mode-state.hook.ts

@ -1,14 +0,0 @@ @@ -1,14 +0,0 @@
import { create } from 'zustand'
import { ChatViewModeEnum } from '../enums'
interface IChatViewModeState {
mode: ChatViewModeEnum
setMode: (mode: ChatViewModeEnum) => void
}
export const useChatViewModeState = create<IChatViewModeState>()(set => ({
mode: ChatViewModeEnum.DEFAULT,
setMode: (mode: ChatViewModeEnum) => {
set({ mode })
},
}))

10
src/modules/chats/hooks/use-chats-list.hook.ts

@ -30,7 +30,10 @@ import { SkeletonDataKey } from '@/services/system' @@ -30,7 +30,10 @@ import { SkeletonDataKey } from '@/services/system'
import { chatManager } from '@/managers'
import { getErrorMessage } from '@/shared/helpers'
export const useChatList = () => {
interface Options {
forceCache?: boolean
}
export const useChatList = (options: Options = {}) => {
const nav = useNav()
const socket = useSocket()
const accountId = useSelector(selectId)
@ -65,7 +68,10 @@ export const useChatList = () => { @@ -65,7 +68,10 @@ export const useChatList = () => {
_setItems,
isInit,
} = useFlatList<IChat>({
fetchItems: chatManager.getChats.bind(chatManager),
fetchItems: params => {
params.params.forceCache = options.forceCache
return chatManager.getChats.bind(chatManager)(params)
},
needInit: false,
clearWhenReload: false,
limit: 10,

13
src/modules/chats/hooks/use-create-text-message.hook.ts

@ -16,22 +16,27 @@ import { ISendTextMessage } from '@/api/chat-messages/requests.interfaces' @@ -16,22 +16,27 @@ import { ISendTextMessage } from '@/api/chat-messages/requests.interfaces'
import { getLinksFromTxt, isImgUrl } from '@/shared/helpers'
import { fsService } from '@/services/system'
import { chatMessageManager } from '@/managers/chat-message.manager'
import { useAsyncState } from '@/shared/hooks/use-async-state.hook'
import { useChatState } from '../chat/states'
interface IProps {
chatMembers: IChatMember[]
chatId: number | string
replyToMessage: IChatMessage
onSend: () => void
}
export const useCreateTextMessage = ({
chatMembers,
chatId,
replyToMessage,
onSend,
}: IProps) => {
const chatId = useChatState(s => s.chatId)
const accountId = useSelector(selectId)
const [message, setMessage] = useState<string>('')
const [message, setMessage, cleanMessage] = useAsyncState(
`chat-message/${chatId}`,
'',
)
const [isSending, setSending] = useState<boolean>(false)
const [editedMessage, setEditedMessage] = useState<IChatMessage>(null)
@ -73,6 +78,7 @@ export const useCreateTextMessage = ({ @@ -73,6 +78,7 @@ export const useCreateTextMessage = ({
onSend()
setMessage('')
cleanMessage()
if (editedMessage) {
await chatMessagesService.editTextMessage({
@ -148,6 +154,7 @@ export const useCreateTextMessage = ({ @@ -148,6 +154,7 @@ export const useCreateTextMessage = ({
)
const allItems = usersCanMention.map(it => {
// eslint-disable-next-line no-unsafe-optional-chaining
const { firstName, lastName, avatarUrl } = it?.user
const name = createFullName(firstName, lastName)
return {

17
src/modules/chats/hooks/use-edit-group-chat.hook.ts

@ -46,6 +46,7 @@ export const useEditGroupChat = (id?: number) => { @@ -46,6 +46,7 @@ export const useEditGroupChat = (id?: number) => {
const [membersIdsToAdd, setMembersIdsToAdd] = useState<number[]>([])
const [role, setRole] = useState<ChatMemberRole>(ChatMemberRole.Admin)
const selectedChatId = useSelector(selectSelectedChatId)
const [isLoading, setLoading] = useState(true)
const nav = useNavigation<TNav>()
@ -53,10 +54,17 @@ export const useEditGroupChat = (id?: number) => { @@ -53,10 +54,17 @@ export const useEditGroupChat = (id?: number) => {
const accountId = useSelector(selectId)
const fetchDetails = async () => {
const chatDetails = await chatManager.getChatDetail.bind(chatManager)({
id: chatId,
})
setChatDetails(chatDetails)
setLoading(true)
try {
const chatDetails = await chatManager.getChatDetail.bind(
chatManager,
)({
id: chatId,
})
setChatDetails(chatDetails)
} finally {
setLoading(false)
}
}
useEffect(() => {
@ -315,5 +323,6 @@ export const useEditGroupChat = (id?: number) => { @@ -315,5 +323,6 @@ export const useEditGroupChat = (id?: number) => {
previewImg,
setPreviewImg,
role,
isLoading,
}
}

10
src/modules/chats/hooks/use-selected-chats.hook.ts

@ -2,6 +2,7 @@ import { chatManager } from '@/managers' @@ -2,6 +2,7 @@ import { chatManager } from '@/managers'
import { IChat, IChatInList, useFlatList } from '@/shared'
import { useEffect, useMemo, useState } from 'react'
import { transformChatToSelectListItem } from '../transforms'
import { SkeletonDataKey } from '@/services/system'
export const useSelectedChats = () => {
const [selectedChats, setSelectedChats] = useState<IChatInList[]>([])
@ -11,10 +12,16 @@ export const useSelectedChats = () => { @@ -11,10 +12,16 @@ export const useSelectedChats = () => {
items: chats,
loadMore,
setLoadParams,
isLoading,
isInit,
isLoadingNext,
} = useFlatList<IChat>({
fetchItems: chatManager.getChats.bind(chatManager),
needInit: true,
clearWhenReload: false,
defaultLoading: false,
defaultItems: [],
skeletonDataKey: SkeletonDataKey.Chats,
})
const transformedChats = useMemo(
@ -56,5 +63,8 @@ export const useSelectedChats = () => { @@ -56,5 +63,8 @@ export const useSelectedChats = () => {
setSearchString,
loadMore,
setSelectedChats,
isLoading,
isInit,
isLoadingNext,
}
}

105
src/modules/chats/hooks/use-selected-messages.hook.ts

@ -1,6 +1,5 @@ @@ -1,6 +1,5 @@
import _ from 'lodash'
import { IChatMessage } from '@/shared/components/plugins/chat'
import { create } from 'zustand'
import {
ChatMemberRole,
MessageType,
@ -13,60 +12,19 @@ import { ChatMessageActionEnum, ChatViewModeEnum } from '../enums' @@ -13,60 +12,19 @@ import { ChatMessageActionEnum, ChatViewModeEnum } from '../enums'
import { getSelectedMessagesMenuOptions } from '../configs'
import { isMessageEqualURL } from '@/shared/helpers'
import { fsService } from '@/services/system'
import { useChatViewModeState } from './use-chat-view-mode-state.hook'
import { chatMessagesService } from '@/services/domain'
import Clipboard from '@react-native-community/clipboard'
import { getCopiedContent } from '../helpers'
import Toast from 'react-native-toast-message'
interface IChatSelectedMessagesState {
messages: IChatMessage[]
selectMessage: (message: IChatMessage) => void
unselectAll: () => void
}
export const useChatSelectedMessagesState =
create<IChatSelectedMessagesState>()(set => ({
messages: [],
selectMessage(message: IChatMessage) {
set(state => {
if (_.find(state.messages, it => it.id === message.id))
return {
messages: state.messages.filter(
it => it.id !== message.id,
),
}
else
return {
messages: [
...state.messages,
_.pick(message, [
'id',
'type',
'content',
'authorId',
'chatId',
'read',
'isMy',
'author',
'isPined',
'createdAt',
]),
],
}
})
},
unselectAll() {
set({ messages: [] })
},
}))
import { useChatSelectedMessagesState } from '../chat/states'
import { chatViewModeStateCtr } from '../chat/states/use-chat-view-mode.state'
import { ChatMessageActionChecker } from '../chat/core/actions'
export const useChatSelectedMessages = () => {
const nav = useNav()
const messages = useChatSelectedMessagesState(s => s.messages)
const { unselectAll } = useChatSelectedMessagesState()
const { setMode } = useChatViewModeState()
const unselectAll = useChatSelectedMessagesState(s => s.unselectAll)
const actions = {
[ChatMessageActionEnum.FORWARD]: forwardMany,
@ -78,7 +36,7 @@ export const useChatSelectedMessages = () => { @@ -78,7 +36,7 @@ export const useChatSelectedMessages = () => {
const afterAction = () => {
unselectAll()
setMode(ChatViewModeEnum.DEFAULT)
chatViewModeStateCtr().setMode(ChatViewModeEnum.DEFAULT)
}
function forwardMany() {
@ -151,54 +109,15 @@ export const useChatSelectedMessages = () => { @@ -151,54 +109,15 @@ export const useChatSelectedMessages = () => {
)
}
const onPressActions = (
userRoleInChat: ChatMemberRole,
isConnected: boolean,
) => {
const canShare =
isConnected &&
messages.length === 1 &&
messages[0].type !== MessageType.Sticker
const canCopy = checkCanCopy()
const canDeleteForAll = checkCanDeleteForAll()
const hasOfflineMessages = _.some(messages, it =>
_.isNaN(Number(it.id)),
)
function checkCanCopy() {
if (!isConnected)
return _.every(messages, it => it.type === MessageType.Text)
if (
messages.length === 1 &&
COPY_ENABLED_MESSAGES_TYPES.includes(messages[0].type)
)
return true
if (
messages.length > 1 &&
_.every(messages, it => it.type === MessageType.Text)
)
return true
return false
}
function checkCanDeleteForAll() {
if (userRoleInChat === ChatMemberRole.Admin) return true
return _.every(messages, it => {
if (!it.read && it.isMy) return true
return false
})
}
const onPressActions = () => {
const chatMessageActionChecker = new ChatMessageActionChecker(messages)
const options = getSelectedMessagesMenuOptions({
canDeleteForAll,
canCopy,
canShare,
hasOfflineMessages,
canDeleteForAll: chatMessageActionChecker.canDeleteForAll(),
canCopy: chatMessageActionChecker.canCopy(),
canShare: chatMessageActionChecker.canShare(),
hasOfflineMessages:
chatMessageActionChecker.isSomeOfflineMessages(),
onPress: (actionType: ChatMessageActionEnum) =>
actions[actionType](messages),
})

22
src/modules/chats/hooks/use-send-files.hook.ts

@ -1,6 +1,5 @@ @@ -1,6 +1,5 @@
import { mediaService } from '@/services/system/media.service'
import _ from 'lodash'
import { Alert } from 'react-native'
import { useState } from 'react'
import { appEvents, IChatMessage } from '@/shared'
import { alertFileSizeExceeded, checkFileSize } from '@/shared/helpers'
@ -11,6 +10,7 @@ import { @@ -11,6 +10,7 @@ import {
} from '@/api/chat-messages/requests.interfaces'
import { getFilesConfig } from '@/store/shared'
import { useSelector } from 'react-redux'
import { chatStateCtr } from '../chat/states'
interface IProps {
replyToMessage: IChatMessage
@ -21,8 +21,9 @@ export const useSendFiles = ({ replyToMessage, onSend }: IProps) => { @@ -21,8 +21,9 @@ export const useSendFiles = ({ replyToMessage, onSend }: IProps) => {
const [isSending, setSending] = useState<boolean>(false)
const { chatFilesSize, chatVideosSize } = useSelector(getFilesConfig)
const handlePressGallery = async (chatId: number) => {
const handlePressGallery = async () => {
try {
const chatId = chatStateCtr().chatId
setSending(true)
const items = await mediaService.openMultiplePicker()
if (_.isEmpty(items)) return
@ -55,8 +56,10 @@ export const useSendFiles = ({ replyToMessage, onSend }: IProps) => { @@ -55,8 +56,10 @@ export const useSendFiles = ({ replyToMessage, onSend }: IProps) => {
}
}
const handlePressFile = async (chatId: number) => {
const handlePressFile = async () => {
try {
const chatId = chatStateCtr().chatId
setSending(true)
const selected = await mediaService.openFilesPicker()
@ -80,6 +83,7 @@ export const useSendFiles = ({ replyToMessage, onSend }: IProps) => { @@ -80,6 +83,7 @@ export const useSendFiles = ({ replyToMessage, onSend }: IProps) => {
if (_.isEmpty(allowed)) return
onSend()
for await (const file of allowed) {
try {
const dataToSend: ISendFileMessages = {
@ -98,8 +102,9 @@ export const useSendFiles = ({ replyToMessage, onSend }: IProps) => { @@ -98,8 +102,9 @@ export const useSendFiles = ({ replyToMessage, onSend }: IProps) => {
}
}
const takePhoto = async (chatId: number) => {
const takePhoto = async () => {
try {
const chatId = chatStateCtr().chatId
setSending(true)
const photo = await mediaService.openCamera({
mediaType: 'photo',
@ -133,7 +138,8 @@ export const useSendFiles = ({ replyToMessage, onSend }: IProps) => { @@ -133,7 +138,8 @@ export const useSendFiles = ({ replyToMessage, onSend }: IProps) => {
}
}
const takeVideo = async (chatId: number) => {
const takeVideo = async () => {
const chatId = chatStateCtr().chatId
const video = await mediaService.launchDeviceCamera()
if (video.size > chatVideosSize * 1000) {
@ -158,16 +164,16 @@ export const useSendFiles = ({ replyToMessage, onSend }: IProps) => { @@ -158,16 +164,16 @@ export const useSendFiles = ({ replyToMessage, onSend }: IProps) => {
}
}
const handlePressCamera = async (chatId: number) => {
const handlePressCamera = async () => {
appEvents.emit('openActionSheet', {
items: [
{
name: 'Зняти фото',
onPress: () => takePhoto(chatId),
onPress: () => takePhoto(),
},
{
name: 'Зняти відео',
onPress: () => takeVideo(chatId),
onPress: () => takeVideo(),
},
],
})

90
src/modules/chats/screens/chat-file-preview.screen.tsx

@ -0,0 +1,90 @@ @@ -0,0 +1,90 @@
import { fsService } from '@/services/system'
import {
Button,
FileType,
IconComponent,
ScreenLayout,
WebviewPlugin,
appEvents,
} from '@/shared'
import { useNavigation, useRoute } from '@react-navigation/native'
import React, { useMemo } from 'react'
import { Platform, StyleSheet, TouchableOpacity, View } from 'react-native'
import PDFView from 'react-native-view-pdf'
export const ChatFilePreviewScreen = () => {
const navigation = useNavigation()
const route = useRoute()
const params: any = route.params
const stopLoading = () => {
appEvents.emit('closeLoaderModal', {})
}
const openByApp = () => {
try {
fsService.previewFile(params.fileUrl)
} catch (e) {
console.log('e', e)
}
}
const renderItem = useMemo(() => {
switch (params?.mimeType) {
case FileType.PDF:
return (
<PDFView
fadeInDuration={250.0}
style={{ flex: 1 }}
resource={params?.fileUrl}
resourceType={'url'}
onLoad={stopLoading}
/>
)
case FileType.TXT:
return (
<WebviewPlugin
url={params?.fileUrl}
renderHtml
onLoaded={stopLoading}
/>
)
default:
return (
<WebviewPlugin
url={`https://docs.google.com/gview?embedded=true&url=${encodeURIComponent(
params?.fileUrl,
)}`}
onLoaded={stopLoading}
/>
)
}
}, [params?.fileUrl])
return (
<ScreenLayout horizontalPadding={0}>
<TouchableOpacity
onPress={navigation.goBack}
style={styles.closeBtn}>
<IconComponent name="xcircle-1" size={32} color="#000" />
</TouchableOpacity>
{renderItem}
{Platform.OS === 'android' ? (
<View style={{ padding: 14 }}>
<Button onPress={openByApp} title="Відкрити в додатку" />
</View>
) : null}
</ScreenLayout>
)
}
const styles = StyleSheet.create({
closeBtn: {
position: 'absolute',
top: 20,
left: 5,
zIndex: 99999,
backgroundColor: 'rgba(0,0,0,.1)',
},
})

52
src/modules/chats/screens/chat.tsx

@ -1,8 +1,7 @@ @@ -1,8 +1,7 @@
import {
ChatMemberRole,
ChatType,
createFullName,
MessageType,
PreviewFileModal,
RouteKey,
useNav,
} from '@/shared'
@ -12,16 +11,13 @@ import { @@ -12,16 +11,13 @@ import {
useChatDetails,
useChatMessages,
useChatSelectedMessages,
useChatViewModeState,
useCreateTextMessage,
usePinedMessages,
useSendFiles,
useSendSticker,
} from '../hooks'
import { useSelector } from 'react-redux'
import { selectSelectedChatId } from '@/store/chats'
import { ChatLayout } from '../layouts'
import { transformMessages } from '../transforms'
import { selectId } from '@/store/account'
import { View, Keyboard } from 'react-native'
import {
@ -35,25 +31,25 @@ import { SelectStickerSmart } from '@/modules/media' @@ -35,25 +31,25 @@ import { SelectStickerSmart } from '@/modules/media'
import { useNetInfo } from '@react-native-community/netinfo'
import { ChatViewModeEnum } from '../enums'
import { ChatHeader } from '../components'
import { useChatViewModeState } from '../chat/states'
import { ChatUserRole } from '../chat/core'
export const ChatConversation = () => {
const nav = useNav()
const accountId = useSelector(selectId)
const selectedChatId = useSelector(selectSelectedChatId)
const [isMenuOpen, setMenuOpen] = useState(false)
const mode = useChatViewModeState(s => s.mode)
const { setMode } = useChatViewModeState()
const { isConnected } = useNetInfo()
const { headerChatInfo, chatDetails, isLoading } =
useChatDetails(selectedChatId)
const { chat: chatDetails, chatId: selectedChatId } = useChatDetails()
const {
messages,
transformedMessages,
resetList,
onLongPress,
loadMore,
@ -65,15 +61,7 @@ export const ChatConversation = () => { @@ -65,15 +61,7 @@ export const ChatConversation = () => {
onPressMessagePreview,
scrollToId,
setScrollToId,
} = useChatMessages(
selectedChatId,
chatDetails?.firstMessageId,
chatDetails?.lastMessageId,
chatDetails?.chatMembers,
setMode,
)
const { onPressActions } = useChatSelectedMessages()
} = useChatMessages()
const onSendMessage = () => {
if (replyTo) setReplyToMessage(null)
@ -87,16 +75,7 @@ export const ChatConversation = () => { @@ -87,16 +75,7 @@ export const ChatConversation = () => {
: replyTo
const role = useMemo(() => {
const member = _.find(
chatDetails?.chatMembers,
member => member.userId === accountId,
)
const role =
member && chatDetails?.type === ChatType.Group
? member.role
: ChatMemberRole.Member
return role
return ChatUserRole.fromAccount().role
}, [chatDetails])
const replyToUserName = useMemo(() => {
@ -133,7 +112,6 @@ export const ChatConversation = () => { @@ -133,7 +112,6 @@ export const ChatConversation = () => {
cancelEdit,
} = useCreateTextMessage({
chatMembers: chatDetails?.chatMembers,
chatId: selectedChatId,
replyToMessage,
onSend: onSendMessage,
})
@ -207,14 +185,7 @@ export const ChatConversation = () => { @@ -207,14 +185,7 @@ export const ChatConversation = () => {
}, [setMenuOpen, isMenuOpen])
return (
<ChatLayout
headerComponent={
<ChatHeader
chatInfo={headerChatInfo}
isLoading={isLoading}
onPressActions={() => onPressActions(role, isConnected)}
/>
}>
<ChatLayout headerComponent={<ChatHeader />}>
<>
<View style={{ flex: 1 }}>
<PinedMessage
@ -228,11 +199,7 @@ export const ChatConversation = () => { @@ -228,11 +199,7 @@ export const ChatConversation = () => {
onProfilePress={onPressMessageAuthor}
onMentionPress={onProfilePress}
onForwardedAuthorPress={onProfilePress}
items={transformMessages(
messages,
chatDetails?.chatMembers,
accountId,
)}
items={transformedMessages}
loadNext={loadMore}
loadPrev={loadEarlier}
userId={accountId}
@ -275,6 +242,7 @@ export const ChatConversation = () => { @@ -275,6 +242,7 @@ export const ChatConversation = () => {
/>
<SelectStickerSmart />
<PreviewFileModal />
</>
</ChatLayout>
)

15
src/modules/chats/screens/chats.screen.tsx

@ -1,18 +1,21 @@ @@ -1,18 +1,21 @@
import React, { FC } from 'react'
import React, { FC, useEffect } from 'react'
import { IRouteParams, PrimaryHeader, RouteKey, ScreenLayout } from '@/shared'
import { HeaderRightBtn } from '../atoms'
import { SmartChatsList } from '../smart-components'
import { chatNavigator } from '../chat/core/chat-navigator'
interface IProps extends IRouteParams {}
export const ChatsScreen: FC<IProps> = ({ navigation }) => {
const onCreateConversation = () => {
export const ChatsScreen: FC<IRouteParams> = ({ navigation }) => {
const goCreateConversation = () => {
navigation.navigate(RouteKey.CreateConversation)
}
useEffect(() => {
chatNavigator.setNavigate(navigation.navigate.bind(navigation))
}, [])
const props = {
title: 'Бесіди',
rightComponent: <HeaderRightBtn onPress={onCreateConversation} />,
rightComponent: <HeaderRightBtn onPress={goCreateConversation} />,
}
return (

6
src/modules/chats/screens/create-conversation.screen.tsx

@ -30,6 +30,7 @@ import { SelectChat } from '@/store/chats' @@ -30,6 +30,7 @@ import { SelectChat } from '@/store/chats'
import { useSelector } from 'react-redux'
import { selectAccount } from '@/store/account'
import _ from 'lodash'
import { chatNavigator } from '../chat/core/chat-navigator'
export const CreateConversationScreen = () => {
const account = useSelector(selectAccount)
@ -53,10 +54,7 @@ export const CreateConversationScreen = () => { @@ -53,10 +54,7 @@ export const CreateConversationScreen = () => {
const chatId = await chatManager.getPersonalChatId({
userId,
})
simpleDispatch(new SelectChat({ id: chatId }))
chatManager.readChat.bind(chatManager)(chatId)
nav.navigate(RouteKey.Conversation)
chatNavigator.navigateToChat(chatId)
} catch (e) {
appEvents.emit('openInfoModal', {
title: 'Сталась помилка!',

6
src/modules/chats/screens/create-personal.screen.tsx

@ -28,6 +28,7 @@ import { simpleDispatch } from '@/store/store-helpers' @@ -28,6 +28,7 @@ import { simpleDispatch } from '@/store/store-helpers'
import { SelectChat } from '@/store/chats'
import { useSelector } from 'react-redux'
import { selectAccount } from '@/store/account'
import { chatNavigator } from '../chat/core/chat-navigator'
export const CreatePersonalScreen = () => {
const account = useSelector(selectAccount)
@ -51,10 +52,7 @@ export const CreatePersonalScreen = () => { @@ -51,10 +52,7 @@ export const CreatePersonalScreen = () => {
const chatId = await chatManager.getPersonalChatId({
userId,
})
simpleDispatch(new SelectChat({ id: chatId }))
chatManager.readChat.bind(chatManager)(chatId)
nav.navigate(RouteKey.Conversation)
chatNavigator.navigateToChat(chatId)
} catch (e) {
appEvents.emit('openInfoModal', {
title: 'Сталась помилка!',

60
src/modules/chats/screens/forward-message.screen.tsx

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
import React, { FC, useMemo } from 'react'
import React, { FC, useMemo, useState } from 'react'
import {
$size,
appEvents,
@ -6,20 +6,18 @@ import { @@ -6,20 +6,18 @@ import {
FooterWithBtn,
IRouteParams,
NotFound,
RouteKey,
ScreenLayout,
SearchWithBtn,
useTheme,
} from '@/shared'
import { FlatList, StyleSheet, View } from 'react-native'
import { ActivityIndicator, FlatList, StyleSheet, View } from 'react-native'
import { SelectChatListItem, SelectedChatsRow } from '../components'
import { PartialTheme } from '@/shared/themes/interfaces'
import { isCloseToBottom } from '@/shared/helpers'
import _ from 'lodash'
import { useSelectedChats } from '../hooks'
import { simpleDispatch } from '@/store/store-helpers'
import { SelectChat } from '@/store/chats'
import { chatMessageManager } from '@/managers/chat-message.manager'
import { chatNavigator } from '../chat/core/chat-navigator'
interface IProps extends IRouteParams {
route: {
@ -36,6 +34,7 @@ export const ForwardMessageScreen: FC<IProps> = ({ @@ -36,6 +34,7 @@ export const ForwardMessageScreen: FC<IProps> = ({
},
}) => {
const { styles } = useTheme(createStyles)
const [isSending, setSending] = useState<boolean>(false)
const {
chats,
@ -46,6 +45,8 @@ export const ForwardMessageScreen: FC<IProps> = ({ @@ -46,6 +45,8 @@ export const ForwardMessageScreen: FC<IProps> = ({
searchString,
setSearchString,
loadMore,
isInit,
isLoadingNext,
} = useSelectedChats()
const renderSelectedChatsRow = useMemo(() => {
@ -60,22 +61,18 @@ export const ForwardMessageScreen: FC<IProps> = ({ @@ -60,22 +61,18 @@ export const ForwardMessageScreen: FC<IProps> = ({
}, [selectedChats])
const forwardMessage = async () => {
const chatsIds = selectedChats.map(it => it.id)
if (_.isEmpty(chatsIds)) return
const messages = _.isArray(messageId)
? (messageId as number[])
: [messageId as number]
if (isSending) return
setSending(true)
try {
for await (const id of messages) {
await chatMessageManager.forwardMessage.bind(
chatMessageManager,
)({
messageId: id,
chatsIds,
})
}
const chatsIds = selectedChats.map(it => it.id)
if (_.isEmpty(chatsIds)) return
const messages = _.isArray(messageId)
? (messageId as number[])
: [messageId as number]
await chatMessageManager.forwardMessages(chatsIds, messages)
navigation.goBack()
@ -84,13 +81,8 @@ export const ForwardMessageScreen: FC<IProps> = ({ @@ -84,13 +81,8 @@ export const ForwardMessageScreen: FC<IProps> = ({
selectedChats[0].type === ChatType.Personal
) {
setTimeout(() => {
navigation.goBack()
simpleDispatch(new SelectChat({ id: chatsIds[0] }))
navigation.navigate(RouteKey.Conversation)
appEvents.emit('onReadChat', { chatId: chatsIds[0] })
chatNavigator.navigateToChat(chatsIds[0])
}, 50)
} else {
}
} catch (e) {
appEvents.emit('openInfoModal', {
@ -98,6 +90,8 @@ export const ForwardMessageScreen: FC<IProps> = ({ @@ -98,6 +90,8 @@ export const ForwardMessageScreen: FC<IProps> = ({
message: 'спробуйте будь-ласка пізніше',
pressButtonText: 'Продовжити',
})
} finally {
setSending(false)
}
}
@ -112,7 +106,11 @@ export const ForwardMessageScreen: FC<IProps> = ({ @@ -112,7 +106,11 @@ export const ForwardMessageScreen: FC<IProps> = ({
},
}}
footer={() => (
<FooterWithBtn btnTitle="Переслати" onPress={forwardMessage} />
<FooterWithBtn
btnTitle="Переслати"
onPress={forwardMessage}
isLoading={isSending}
/>
)}
horizontalPadding={0}>
{selectedChats.length ? renderSelectedChatsRow : null}
@ -150,7 +148,15 @@ export const ForwardMessageScreen: FC<IProps> = ({ @@ -150,7 +148,15 @@ export const ForwardMessageScreen: FC<IProps> = ({
isDisabled={item.isDisabled}
/>
)}
ListEmptyComponent={<NotFound text="Бесіди не знайдені" />}
ListFooterComponent={() => {
if (isInit && !chats.length) {
return <NotFound text="Бесіди не знайдені" />
}
if (isLoadingNext) {
return <ActivityIndicator />
}
}}
/>
</View>
</ScreenLayout>

70
src/modules/chats/screens/group-chat-detail.screen.tsx

@ -7,22 +7,15 @@ import { @@ -7,22 +7,15 @@ import {
} from '@/shared'
import { simpleDispatch } from '@/store/store-helpers'
import { UnselectAllUsers } from '@/store/users'
import React, { FC, useCallback, useState } from 'react'
import React, { FC } from 'react'
import { ActivityIndicator, StyleSheet, View } from 'react-native'
import { ChatDetailFooter, CreateChatHeaderComponent } from '../components'
import { useEditGroupChat } from '../hooks'
import { ChatMembersListSmart } from '../smart-components'
import { useChatState } from '../chat/states'
interface IProps extends IRouteParams {
route: {
params: { chatId: number }
}
}
export const GroupChatDetailScreen: FC<IProps> = ({ navigation, route }) => {
const chatId = route?.params?.chatId || null
const { styles, theme } = useTheme(createStyles)
const [heightList, seHeight] = useState(null)
export const GroupChatDetailScreen: FC<IRouteParams> = ({ navigation }) => {
const chatId = useChatState(s => s.chatId)
const {
chatInfo,
isChatMuted,
@ -35,6 +28,7 @@ export const GroupChatDetailScreen: FC<IProps> = ({ navigation, route }) => { @@ -35,6 +28,7 @@ export const GroupChatDetailScreen: FC<IProps> = ({ navigation, route }) => {
onSubmit,
setPreviewImg,
role,
isLoading,
} = useEditGroupChat(chatId)
const goBack = () => {
@ -42,50 +36,32 @@ export const GroupChatDetailScreen: FC<IProps> = ({ navigation, route }) => { @@ -42,50 +36,32 @@ export const GroupChatDetailScreen: FC<IProps> = ({ navigation, route }) => {
navigation.goBack()
}
const findDimesions = useCallback(
layout => {
if (chatInfo?.type === ChatType.Group) {
const { height } = layout
seHeight(height)
}
},
[chatInfo?.type, ChatType.Group],
)
const listChatMembersLayout = () => {
if (chatInfo?.type === ChatType.Group && !heightList) {
return (
<View
style={{
flexGrow: 1,
alignItems: 'center',
justifyContent: 'center',
}}
onLayout={e => findDimesions(e.nativeEvent.layout)}>
<ActivityIndicator color={theme.$loaderPrimary} />
</View>
)
} else {
return (
<View
style={{
height: heightList,
}}>
<ChatMembersListSmart
userRole={role}
members={chatMembers}
ignoreRole={chatInfo?.type === ChatType.Personal}
/>
</View>
)
if (isLoading) {
return <ActivityIndicator />
}
return (
<View style={{ flex: 1 }}>
<ChatMembersListSmart
userRole={role}
members={chatMembers}
ignoreRole={chatInfo?.type === ChatType.Personal}
/>
</View>
)
}
return (
<ScreenLayout
header={{ title: 'Редагувати чат', goBack }}
horizontalPadding={0}>
<View style={[{ height: '100%', flexDirection: 'column' }]}>
<View
style={{
height: '100%',
flexDirection: 'column',
justifyContent: 'space-between',
}}>
{chatInfo?.type === ChatType.Group && (
<CreateChatHeaderComponent
value={groupTheme}

25
src/modules/chats/smart-components/chats-list.smart-component.tsx

@ -1,23 +1,16 @@ @@ -1,23 +1,16 @@
import React, { FC, useCallback } from 'react'
import { $size, appEvents, IChat, RouteKey } from '@/shared'
import { $size, IChat } from '@/shared'
import { SearchForm } from '@/shared/components/forms'
import { useTheme } from '@/shared/hooks/use-theme.hook'
import { SelectChat } from '@/store/chats'
import { simpleDispatch } from '@/store/store-helpers'
import { useFocusEffect, useNavigation } from '@react-navigation/native'
import { StackNavigationProp } from '@react-navigation/stack'
import { useFocusEffect } from '@react-navigation/native'
import { Platform, StyleSheet } from 'react-native'
import { ChatsList } from '../components/chats-list.component'
import { useChatList } from '../hooks'
import { chatManager } from '@/managers'
type TConversationNav = StackNavigationProp<{ [RouteKey.Conversation] }>
import { chatNavigator } from '../chat/core/chat-navigator'
export const SmartChatsList: FC = () => {
const { styles } = useTheme(createStyles)
const { navigate } = useNavigation<TConversationNav>()
const {
chats,
searchString,
@ -28,7 +21,7 @@ export const SmartChatsList: FC = () => { @@ -28,7 +21,7 @@ export const SmartChatsList: FC = () => {
isLoadingNext,
resetFlatList,
isInit,
} = useChatList()
} = useChatList({ forceCache: true })
useFocusEffect(
useCallback(() => {
@ -37,15 +30,7 @@ export const SmartChatsList: FC = () => { @@ -37,15 +30,7 @@ export const SmartChatsList: FC = () => {
)
const onPressItem = (item: IChat) => {
simpleDispatch(new SelectChat({ id: item.id }))
appEvents.emit('onReadChat', {
chatId: item.id,
unreadCount: item.unreadMessagesCount,
})
chatManager.readChat.bind(chatManager)(item.id)
navigate(RouteKey.Conversation)
chatNavigator.navigateToChat(item.id, item.unreadMessagesCount)
}
return (

9
src/modules/chats/transforms/chat-messages.transforms.ts

@ -53,6 +53,10 @@ const getNewMembersMessage = (item: IMessageInChat, members: IChatMember[]) => { @@ -53,6 +53,10 @@ const getNewMembersMessage = (item: IMessageInChat, members: IChatMember[]) => {
getUserName(it, members),
)
if (!Array.isArray(newMembersNames) || !newMembersNames.length) {
return `Користувач ${name} додав нових учасників у чат`
}
return `Користувач ${name} додав ${newMembersNames.join(', ')} у чат`
}
@ -118,6 +122,8 @@ const getContentByMessageType = { @@ -118,6 +122,8 @@ const getContentByMessageType = {
}
const createContent = (item: IMessageInChat, members: IChatMember[]) => {
if (!item) return null
if (isSystem(item.type))
return getContentByMessageType[item.type](item, members)
@ -145,9 +151,10 @@ const createContent = (item: IMessageInChat, members: IChatMember[]) => { @@ -145,9 +151,10 @@ const createContent = (item: IMessageInChat, members: IChatMember[]) => {
}
const getForwardedFrom = (item: IMessageInChat) => {
if (!item?.content?.originalMessage) return null
if (item.type !== MessageType.Forwarded) return null
const { user } = item.content?.originalMessage
const { user } = item.content?.originalMessage as any
return {
id: user.id,
name: createFullName(user?.firstName, user?.lastName),

6
src/modules/contacts/screens/contact-detail.screen.tsx

@ -22,6 +22,7 @@ import { simpleDispatch } from '@/store/store-helpers' @@ -22,6 +22,7 @@ import { simpleDispatch } from '@/store/store-helpers'
import { SelectChat } from '@/store/chats'
import { chatManager } from '@/managers'
import { callService } from '@/modules/calls/services'
import { chatNavigator } from '@/modules/chats/chat/core/chat-navigator'
interface IProps extends IRouteParams {
route: {
@ -44,10 +45,7 @@ export const ContactDetailScreen: FC<IProps> = ({ navigation, route }) => { @@ -44,10 +45,7 @@ export const ContactDetailScreen: FC<IProps> = ({ navigation, route }) => {
{ userId: contact?.userId },
contact?.chatId,
)
simpleDispatch(new SelectChat({ id: chatId }))
chatManager.readChat.bind(chatManager)(chatId)
navigation.navigate(RouteKey.Conversation)
chatNavigator.navigateToChat(chatId)
} catch (e) {
appEvents.emit('openInfoModal', {
title: 'Сталась помилка!',

8
src/modules/contacts/screens/contacts.screen.tsx

@ -23,12 +23,13 @@ export const ContactScreen: FC = () => { @@ -23,12 +23,13 @@ export const ContactScreen: FC = () => {
const renderScene = SceneMap({
contacts: ContactsSmartList,
calls: CallSmartList,
calls: () => <CallSmartList />,
})
const renderTabBar = () => (
<SwitchButtons
style={styles.switchContainer}
activeItem={index}
tabs={[
{
label: 'Контакти',
@ -54,7 +55,10 @@ export const ContactScreen: FC = () => { @@ -54,7 +55,10 @@ export const ContactScreen: FC = () => {
renderTabBar={renderTabBar}
navigationState={{ index, routes }}
renderScene={renderScene}
onIndexChange={setIndex}
onIndexChange={index => {
console.log('onindex changed', index)
setIndex(index)
}}
/>
</ScreenLayout>
)

2
src/modules/contacts/smart-component/contacts-list.smart-component.tsx

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
import React, { useCallback, useState } from 'react'
import { $size, RouteKey } from '@/shared'
import { $size, RouteKey, useSocketListener } from '@/shared'
import { useTheme } from '@/shared/hooks/use-theme.hook'
import { useNavigation } from '@react-navigation/native'
import {

1
src/modules/root/hooks/use-app-badge.hook.ts

@ -119,6 +119,7 @@ export const useAppBadge = () => { @@ -119,6 +119,7 @@ export const useAppBadge = () => {
await tasksService.getUnreadTasksCount()
const onChatIsRead = async (data: SocketEvents['chat/is-read']) => {
console.log('chatWasReaddeed', data)
if (data.userId === account.id)
setTimeout(async () => await reloadUnreadMessagesCount(), 1000)
}

2
src/modules/root/hooks/use-net-connect.hook.ts

@ -3,6 +3,7 @@ import { appEvents } from '@/shared' @@ -3,6 +3,7 @@ import { appEvents } from '@/shared'
import NetInfo, { useNetInfo } from '@react-native-community/netinfo'
import _ from 'lodash'
import { useEffect, useState } from 'react'
import { netConnectStateCtr } from '../states/net-connect.state'
export const useNetConnect = () => {
const { isConnected } = useNetInfo(null)
@ -59,6 +60,7 @@ export const useNetConnect = () => { @@ -59,6 +60,7 @@ export const useNetConnect = () => {
useEffect(() => {
internetConnectionHandler()
netConnectStateCtr().onChange(isConnected)
}, [isConnected])
return {

4
src/modules/root/index.tsx

@ -39,6 +39,7 @@ import { StyleSheet } from 'react-native' @@ -39,6 +39,7 @@ import { StyleSheet } from 'react-native'
import { PartialTheme } from '@/shared/themes/interfaces'
import { useAppBadge, useAppSocketListener, useNetConnect } from './hooks'
import { PreviewFileModal, LoaderModal } from '@/shared/components/modals'
import { IncomingCallWidget } from '../calls/widgets'
export const Navigation: FC = () => {
const activeModule = useSelector(selectActiveNavigationModule)
@ -91,6 +92,7 @@ export const Navigation: FC = () => { @@ -91,6 +92,7 @@ export const Navigation: FC = () => {
<SelectTaxonomiesModalSmart />
<RecordAudioModalSmart />
<ChatSendImgModal />
<IncomingCallWidget />
</>
)
}
@ -114,7 +116,7 @@ export const Navigation: FC = () => { @@ -114,7 +116,7 @@ export const Navigation: FC = () => {
<ActionSheet />
<FancyboxSmart />
<FullscreenVideoSmart />
<PreviewFileModal />
<LoaderModal />
{GlobalComponents}
</>

3
src/modules/root/navigation-groups/tab-bar.group.tsx

@ -1,15 +1,12 @@ @@ -1,15 +1,12 @@
import React, { FC, useEffect } from 'react'
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'
import { TabBarSmart } from '../smart-components/tab-bar.smart-component'
import { SocketIo } from '@/services/system/real-time.service'
import { RouteKey } from '@/shared'
import { HomeScreen } from '@/modules/home'
import { ChatsScreen } from '@/modules/chats'
import { ContactScreen } from '@/modules/contacts'
import { SettingsScreen } from '@/modules/settings'
import { AddUpdateTaskScreen } from '@/modules/tasks'
import { Comments } from '@/modules/comments/screens/comments.screen'
import { HomeGroup } from './home.group'
const Tab = createBottomTabNavigator()

5
src/modules/root/navigation-groups/users.group.tsx

@ -30,6 +30,7 @@ import { @@ -30,6 +30,7 @@ import {
import { CallScreen } from '@/modules/calls/screens/call'
import { callKeepService } from '@/modules/calls/services'
import { Platform } from 'react-native'
import { ChatFilePreviewScreen } from '@/modules/chats/screens/chat-file-preview.screen'
const Stack = createNativeStackNavigator()
@ -67,6 +68,10 @@ export const UsersGroup: FC = () => { @@ -67,6 +68,10 @@ export const UsersGroup: FC = () => {
name={RouteKey.Conversation}
component={ChatConversation}
/>
<Stack.Screen
name={RouteKey.ChatFilePreview}
component={ChatFilePreviewScreen}
/>
<Stack.Screen
name={RouteKey.GroupChatDetail}
component={GroupChatDetailScreen}

1
src/modules/root/states/index.ts

@ -0,0 +1 @@ @@ -0,0 +1 @@
export * from './net-connect.state'

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save