From 03fce5ec9c10529ab0e01ac5c355a5c31431ed41 Mon Sep 17 00:00:00 2001 From: Anton Stjernquist Date: Fri, 20 Sep 2024 22:55:28 +0200 Subject: [PATCH] feat(messages): message notifications & update react-fivem-hooks --- pnpm-lock.yaml | 247 +++++++++++++++++- src/server/database/schemas/Message.ts | 3 - src/server/index.ts | 2 +- src/server/repositories/CallRepository.ts | 33 ++- src/server/repositories/MessageRepository.ts | 17 +- src/server/router/devices.ts | 2 +- src/server/services/MessageService.ts | 1 - src/shared/Types.ts | 12 + src/ui/package.json | 2 +- src/ui/src/App.tsx | 80 +++++- src/ui/src/Apps/Calls/Contacts/index.tsx | 6 +- .../Calls/Messages/Conversation/index.tsx | 7 +- src/ui/src/Apps/Calls/Messages/New/index.tsx | 23 ++ src/ui/src/Apps/Calls/Messages/index.tsx | 14 +- src/ui/src/Providers.tsx | 2 +- src/ui/src/api/device.ts | 7 +- src/ui/src/api/hooks/useActiveCall.ts | 20 +- src/ui/src/api/hooks/useContactPhoneNumber.ts | 2 +- .../src/api/hooks/useMessagesNotifications.ts | 26 ++ .../src/components/Forms/NewMessageForm.tsx | 76 ++++++ .../Notification/NotificationsPopupList.tsx | 2 +- .../NotificationContext/useNotifications.tsx | 9 + src/ui/src/routes.tsx | 7 + 23 files changed, 560 insertions(+), 40 deletions(-) create mode 100644 src/ui/src/Apps/Calls/Messages/New/index.tsx create mode 100644 src/ui/src/api/hooks/useMessagesNotifications.ts create mode 100644 src/ui/src/components/Forms/NewMessageForm.tsx diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 408f6ebca..971f6c4a0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -143,8 +143,8 @@ importers: specifier: ^2.0.10 version: 2.0.10(react@18.3.1) react-fivem-hooks: - specifier: ^1.0.3 - version: 1.0.3 + specifier: ^1.0.4 + version: 1.0.4 react-router: specifier: ^6.26.1 version: 6.26.1(react@18.3.1) @@ -635,6 +635,15 @@ packages: - typescript dev: true + /@esbuild/aix-ppc64@0.19.12: + resolution: {integrity: sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + requiresBuild: true + dev: false + optional: true + /@esbuild/aix-ppc64@0.21.5: resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} engines: {node: '>=12'} @@ -651,6 +660,15 @@ packages: requiresBuild: true optional: true + /@esbuild/android-arm64@0.19.12: + resolution: {integrity: sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + requiresBuild: true + dev: false + optional: true + /@esbuild/android-arm64@0.21.5: resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} engines: {node: '>=12'} @@ -676,6 +694,15 @@ packages: dev: true optional: true + /@esbuild/android-arm@0.19.12: + resolution: {integrity: sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + requiresBuild: true + dev: false + optional: true + /@esbuild/android-arm@0.21.5: resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} engines: {node: '>=12'} @@ -692,6 +719,15 @@ packages: requiresBuild: true optional: true + /@esbuild/android-x64@0.19.12: + resolution: {integrity: sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + requiresBuild: true + dev: false + optional: true + /@esbuild/android-x64@0.21.5: resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} engines: {node: '>=12'} @@ -708,6 +744,15 @@ packages: requiresBuild: true optional: true + /@esbuild/darwin-arm64@0.19.12: + resolution: {integrity: sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: false + optional: true + /@esbuild/darwin-arm64@0.21.5: resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} engines: {node: '>=12'} @@ -724,6 +769,15 @@ packages: requiresBuild: true optional: true + /@esbuild/darwin-x64@0.19.12: + resolution: {integrity: sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: false + optional: true + /@esbuild/darwin-x64@0.21.5: resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} engines: {node: '>=12'} @@ -740,6 +794,15 @@ packages: requiresBuild: true optional: true + /@esbuild/freebsd-arm64@0.19.12: + resolution: {integrity: sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + requiresBuild: true + dev: false + optional: true + /@esbuild/freebsd-arm64@0.21.5: resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} engines: {node: '>=12'} @@ -756,6 +819,15 @@ packages: requiresBuild: true optional: true + /@esbuild/freebsd-x64@0.19.12: + resolution: {integrity: sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + requiresBuild: true + dev: false + optional: true + /@esbuild/freebsd-x64@0.21.5: resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} engines: {node: '>=12'} @@ -772,6 +844,15 @@ packages: requiresBuild: true optional: true + /@esbuild/linux-arm64@0.19.12: + resolution: {integrity: sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: false + optional: true + /@esbuild/linux-arm64@0.21.5: resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} engines: {node: '>=12'} @@ -788,6 +869,15 @@ packages: requiresBuild: true optional: true + /@esbuild/linux-arm@0.19.12: + resolution: {integrity: sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: false + optional: true + /@esbuild/linux-arm@0.21.5: resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} engines: {node: '>=12'} @@ -804,6 +894,15 @@ packages: requiresBuild: true optional: true + /@esbuild/linux-ia32@0.19.12: + resolution: {integrity: sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + requiresBuild: true + dev: false + optional: true + /@esbuild/linux-ia32@0.21.5: resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} engines: {node: '>=12'} @@ -829,6 +928,15 @@ packages: dev: true optional: true + /@esbuild/linux-loong64@0.19.12: + resolution: {integrity: sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + requiresBuild: true + dev: false + optional: true + /@esbuild/linux-loong64@0.21.5: resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} engines: {node: '>=12'} @@ -845,6 +953,15 @@ packages: requiresBuild: true optional: true + /@esbuild/linux-mips64el@0.19.12: + resolution: {integrity: sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + requiresBuild: true + dev: false + optional: true + /@esbuild/linux-mips64el@0.21.5: resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} engines: {node: '>=12'} @@ -861,6 +978,15 @@ packages: requiresBuild: true optional: true + /@esbuild/linux-ppc64@0.19.12: + resolution: {integrity: sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + requiresBuild: true + dev: false + optional: true + /@esbuild/linux-ppc64@0.21.5: resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} engines: {node: '>=12'} @@ -877,6 +1003,15 @@ packages: requiresBuild: true optional: true + /@esbuild/linux-riscv64@0.19.12: + resolution: {integrity: sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + requiresBuild: true + dev: false + optional: true + /@esbuild/linux-riscv64@0.21.5: resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} engines: {node: '>=12'} @@ -893,6 +1028,15 @@ packages: requiresBuild: true optional: true + /@esbuild/linux-s390x@0.19.12: + resolution: {integrity: sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + requiresBuild: true + dev: false + optional: true + /@esbuild/linux-s390x@0.21.5: resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} engines: {node: '>=12'} @@ -909,6 +1053,15 @@ packages: requiresBuild: true optional: true + /@esbuild/linux-x64@0.19.12: + resolution: {integrity: sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: false + optional: true + /@esbuild/linux-x64@0.21.5: resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} engines: {node: '>=12'} @@ -925,6 +1078,15 @@ packages: requiresBuild: true optional: true + /@esbuild/netbsd-x64@0.19.12: + resolution: {integrity: sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + requiresBuild: true + dev: false + optional: true + /@esbuild/netbsd-x64@0.21.5: resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} engines: {node: '>=12'} @@ -949,6 +1111,15 @@ packages: requiresBuild: true optional: true + /@esbuild/openbsd-x64@0.19.12: + resolution: {integrity: sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + requiresBuild: true + dev: false + optional: true + /@esbuild/openbsd-x64@0.21.5: resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} engines: {node: '>=12'} @@ -965,6 +1136,15 @@ packages: requiresBuild: true optional: true + /@esbuild/sunos-x64@0.19.12: + resolution: {integrity: sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + requiresBuild: true + dev: false + optional: true + /@esbuild/sunos-x64@0.21.5: resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} engines: {node: '>=12'} @@ -981,6 +1161,15 @@ packages: requiresBuild: true optional: true + /@esbuild/win32-arm64@0.19.12: + resolution: {integrity: sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: false + optional: true + /@esbuild/win32-arm64@0.21.5: resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} engines: {node: '>=12'} @@ -997,6 +1186,15 @@ packages: requiresBuild: true optional: true + /@esbuild/win32-ia32@0.19.12: + resolution: {integrity: sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: false + optional: true + /@esbuild/win32-ia32@0.21.5: resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} engines: {node: '>=12'} @@ -1013,6 +1211,15 @@ packages: requiresBuild: true optional: true + /@esbuild/win32-x64@0.19.12: + resolution: {integrity: sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: false + optional: true + /@esbuild/win32-x64@0.21.5: resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} engines: {node: '>=12'} @@ -3397,6 +3604,37 @@ packages: esbuild-windows-arm64: 0.15.15 dev: true + /esbuild@0.19.12: + resolution: {integrity: sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==} + engines: {node: '>=12'} + hasBin: true + requiresBuild: true + optionalDependencies: + '@esbuild/aix-ppc64': 0.19.12 + '@esbuild/android-arm': 0.19.12 + '@esbuild/android-arm64': 0.19.12 + '@esbuild/android-x64': 0.19.12 + '@esbuild/darwin-arm64': 0.19.12 + '@esbuild/darwin-x64': 0.19.12 + '@esbuild/freebsd-arm64': 0.19.12 + '@esbuild/freebsd-x64': 0.19.12 + '@esbuild/linux-arm': 0.19.12 + '@esbuild/linux-arm64': 0.19.12 + '@esbuild/linux-ia32': 0.19.12 + '@esbuild/linux-loong64': 0.19.12 + '@esbuild/linux-mips64el': 0.19.12 + '@esbuild/linux-ppc64': 0.19.12 + '@esbuild/linux-riscv64': 0.19.12 + '@esbuild/linux-s390x': 0.19.12 + '@esbuild/linux-x64': 0.19.12 + '@esbuild/netbsd-x64': 0.19.12 + '@esbuild/openbsd-x64': 0.19.12 + '@esbuild/sunos-x64': 0.19.12 + '@esbuild/win32-arm64': 0.19.12 + '@esbuild/win32-ia32': 0.19.12 + '@esbuild/win32-x64': 0.19.12 + dev: false + /esbuild@0.21.5: resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} engines: {node: '>=12'} @@ -5540,9 +5778,10 @@ packages: react: 18.3.1 dev: false - /react-fivem-hooks@1.0.3: - resolution: {integrity: sha512-E491biV13pavDHQWfu1unLeW6tphtiqcxw2AY1NMqTzVOLXme/pZQFmOlexXL3fNoInzLiZoxZmw9/KFWE+8Kg==} + /react-fivem-hooks@1.0.4: + resolution: {integrity: sha512-QL/0zBJlp6lAx4XkkFkSGuV4cfKi5E8cDbMsEsqhLGhV+4WASpXO9fKzbG8v7iFSZ8uIENDV4fEGNZQlb2e1RQ==} dependencies: + esbuild: 0.19.12 react: 17.0.2 react-dom: 17.0.2(react@17.0.2) typescript: 4.9.5 diff --git a/src/server/database/schemas/Message.ts b/src/server/database/schemas/Message.ts index d781b3c22..d27ee6910 100644 --- a/src/server/database/schemas/Message.ts +++ b/src/server/database/schemas/Message.ts @@ -1,8 +1,5 @@ import { createDbTable, DATABASE_PREFIX } from '../utils'; import { DBInstance } from '../knex'; -import { Message } from '../../../shared/Types'; - -export type InsertMessage = Pick; export const createMessageTable = () => { createDbTable('message', (table) => { diff --git a/src/server/index.ts b/src/server/index.ts index f5225c289..9a070a439 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -22,7 +22,7 @@ function bootstrap() { initDB(); const app = new App({ - debug: false, + debug: true, listeners: { server: SERVER_EVENT_LISTENER, client: CLIENT_EVENT_LISTENER, diff --git a/src/server/repositories/CallRepository.ts b/src/server/repositories/CallRepository.ts index ed3484c48..09620b082 100644 --- a/src/server/repositories/CallRepository.ts +++ b/src/server/repositories/CallRepository.ts @@ -1,4 +1,4 @@ -import { Call } from '../../shared/Types'; +import { Call, CallWithPhoneNumbers } from '../../shared/Types'; import { DBInstance } from '../database/knex'; import { InsertCall } from '../database/schemas/Call'; @@ -29,7 +29,21 @@ class CallRepository { public async createCall(call: InsertCall): Promise { const [newId] = await DBInstance(tableName).insert(call); - return await DBInstance(tableName).select('*').where('id', newId).first(); + return await DBInstance(tableName) + .select('*') + .where('tmp_phone_call.id', newId) + .leftJoin('tmp_phone_sim_card', 'tmp_phone_sim_card.id', 'tmp_phone_call.receiver_id') + .leftJoin( + 'tmp_phone_sim_card as tmp_phone_sim_card2', + 'tmp_phone_sim_card2.id', + 'tmp_phone_call.caller_id', + ) + .select( + 'tmp_phone_call.*', + 'tmp_phone_sim_card.phone_number as receiver_phone_number', + 'tmp_phone_sim_card2.phone_number as caller_phone_number', + ) + .first(); } public async getPendingCalls(simCardId: number): Promise { @@ -48,12 +62,23 @@ class CallRepository { .andWhere('acknowledged_at', null); } - public async getActiveCalls(simCardId: number): Promise { + public async getActiveCalls(simCardId: number): Promise { return await DBInstance(tableName) .where('ended_at', null) .andWhere(function () { this.where('receiver_id', simCardId).orWhere('caller_id', simCardId); - }); + }) + .leftJoin('tmp_phone_sim_card', 'tmp_phone_sim_card.id', 'tmp_phone_call.receiver_id') + .leftJoin( + 'tmp_phone_sim_card as tmp_phone_sim_card2', + 'tmp_phone_sim_card2.id', + 'tmp_phone_call.caller_id', + ) + .select( + 'tmp_phone_call.*', + 'tmp_phone_sim_card.phone_number as receiver_phone_number', + 'tmp_phone_sim_card2.phone_number as caller_phone_number', + ); } public async updateCall(call: Call): Promise { diff --git a/src/server/repositories/MessageRepository.ts b/src/server/repositories/MessageRepository.ts index 5c02e268b..833bb0bd4 100644 --- a/src/server/repositories/MessageRepository.ts +++ b/src/server/repositories/MessageRepository.ts @@ -1,6 +1,5 @@ -import { Message, MessageWithPhoneNumbers } from '../../shared/Types'; +import { InsertMessage, Message, MessageWithPhoneNumbers } from '../../shared/Types'; import { DBInstance } from '../database/knex'; -import { InsertMessage } from '../database/schemas/Message'; const tableName = 'tmp_phone_message'; @@ -46,9 +45,19 @@ class MessageRepository { .orderBy('created_at', 'asc'); } - public async createMessage(message: InsertMessage): Promise { + public async createMessage(message: InsertMessage): Promise { const [newId] = await DBInstance(tableName).insert(message); - return await DBInstance(tableName).select('*').where('id', newId).first(); + return await DBInstance(tableName) + .select('*') + .where(`${tableName}.id`, newId) + .leftJoin('tmp_phone_sim_card as sender', 'sender.id', 'tmp_phone_message.sender_id') + .leftJoin('tmp_phone_sim_card as receiver', 'receiver.id', 'tmp_phone_message.receiver_id') + .select( + 'tmp_phone_message.*', + 'sender.phone_number as sender_phone_number', + 'receiver.phone_number as receiver_phone_number', + ) + .first(); } public async updateMessage(message: Message): Promise { diff --git a/src/server/router/devices.ts b/src/server/router/devices.ts index 7059a4b7c..732e9835d 100644 --- a/src/server/router/devices.ts +++ b/src/server/router/devices.ts @@ -44,7 +44,7 @@ devicesRouter.add('/register', async (ctx, next) => { const device = await DeviceService.createDevice({ sim_card_id: simCard.id, - identifier: ctx.deviceIdentifier, + identifier: ctx.device.identifier, }); console.log('current-device:updated', device); diff --git a/src/server/services/MessageService.ts b/src/server/services/MessageService.ts index 7c969d797..daf33834b 100644 --- a/src/server/services/MessageService.ts +++ b/src/server/services/MessageService.ts @@ -1,5 +1,4 @@ import { RouterContext } from 'fivem-router'; -import { InsertMessage } from '../database/schemas/Message'; import MessageRepository from '../repositories/MessageRepository'; import SimCardRepository from '../repositories/SimCardRepository'; import { diff --git a/src/shared/Types.ts b/src/shared/Types.ts index 1ada8c6cf..e3c932f21 100644 --- a/src/shared/Types.ts +++ b/src/shared/Types.ts @@ -36,6 +36,11 @@ export interface Call extends Record { acknowledged_at?: Date; } +export interface CallWithPhoneNumbers extends Call { + caller_phone_number: string; + receiver_phone_number: string; +} + export interface Message extends Record { id: number; sender_id: number; @@ -45,6 +50,13 @@ export interface Message extends Record { updated_at: Date; } +export type InsertMessage = Pick; + +export interface MessageWithPhoneNumbers extends Message { + sender_phone_number: string; + receiver_phone_number: string; +} + export interface MessageWithPhoneNumbers extends Message { sender_phone_number: string; receiver_phone_number: string; diff --git a/src/ui/package.json b/src/ui/package.json index 216975d48..17b05e9d6 100644 --- a/src/ui/package.json +++ b/src/ui/package.json @@ -24,7 +24,7 @@ "react": "^18.3.1", "react-dom": "^18.3.1", "react-feather": "^2.0.10", - "react-fivem-hooks": "^1.0.3", + "react-fivem-hooks": "^1.0.4", "react-router": "^6.26.1", "react-router-dom": "^6.26.1", "tailwind-merge": "^2.5.2", diff --git a/src/ui/src/App.tsx b/src/ui/src/App.tsx index af713ff6f..89306ff55 100644 --- a/src/ui/src/App.tsx +++ b/src/ui/src/App.tsx @@ -2,19 +2,21 @@ import { Outlet, useLocation, useNavigate } from 'react-router-dom'; import { Frame } from './Frame'; import { motion, useMotionValue, useTransform } from 'framer-motion'; -import { useEffect } from 'react'; +import { Component, ReactNode, useEffect } from 'react'; import { useNuiEvent } from 'react-fivem-hooks'; import { useCurrentDevice } from './api/hooks/useCurrentDevice'; import { closePhone } from './api/phone'; import { Footer } from './components/Main/Footer'; import { Header } from './components/Main/Header'; +import { useDisableNavigation } from './contexts/NavigationContext'; import { useBroadcastEvent } from './hooks/useBroadcastEvent'; +import { useKeys } from './hooks/useKeys'; import { useThemeType } from './hooks/useTheme'; import { queryClient } from './Providers'; import { isEnvBrowser } from './utils/game'; import { setTheme, Theme } from './utils/theme'; -import { useKeys } from './hooks/useKeys'; -import { useDisableNavigation } from './contexts/NavigationContext'; +import { useActiveCall } from './api/hooks/useActiveCall'; +import { useMessagesNotifications } from './api/hooks/useMessagesNotifications'; export const lightTheme: Theme = { type: 'light', @@ -41,6 +43,9 @@ export const darkTheme: Theme = { }; function App() { + useActiveCall(); + useMessagesNotifications(); + const location = useLocation(); const navigate = useNavigate(); const currentDevice = useCurrentDevice(); @@ -52,6 +57,20 @@ function App() { const borderRadius = useTransform(y, [-150, -40, 0], [40, 0, 0]); const currentThemeType = useThemeType(); + useEffect(() => { + const handleMessage = (event) => { + console.log('----------'); + console.log('message received'); + console.log(event); + console.log('----------'); + }; + + window.addEventListener('message', handleMessage); + return () => { + window.removeEventListener('message', handleMessage); + }; + }, []); + useEffect(() => { if (currentThemeType === 'dark') { setTheme(darkTheme); @@ -127,4 +146,57 @@ function App() { ); } -export default App; +interface ErrorBoundaryProps { + fallback: ReactNode; + children: ReactNode; +} +class ErrorBoundary extends Component { + state: { hasError: boolean }; + + constructor(props: ErrorBoundaryProps) { + super(props); + this.state = { hasError: false }; + } + + static getDerivedStateFromError(error: Error) { + console.error('ErrorBoundary caught an error:', error); + // Update state so the next render will show the fallback UI. + return { hasError: true }; + } + + componentDidCatch(error: Error, info: unknown) { + // Example "componentStack": + // in ComponentThatThrows (created by App) + // in ErrorBoundary (created by App) + // in div (created by App) + // in App + console.error('ErrorBoundary caught an error:', error, info); + // logErrorToMyService(error, info.componentStack); + } + + render() { + if (this.state.hasError) { + // You can render any custom fallback UI + return this.props.fallback; + } + + return this.props.children; + } +} + +const AppWithErrorBoundary = () => { + return ( + +

Something went wrong.

+

Check the console for more information.

+ + } + > + +
+ ); +}; + +export default AppWithErrorBoundary; diff --git a/src/ui/src/Apps/Calls/Contacts/index.tsx b/src/ui/src/Apps/Calls/Contacts/index.tsx index 30d329d76..6c9a5346a 100644 --- a/src/ui/src/Apps/Calls/Contacts/index.tsx +++ b/src/ui/src/Apps/Calls/Contacts/index.tsx @@ -6,11 +6,15 @@ import { Button } from '@/components/ui/button'; export const ContactsView = () => { const device = useCurrentDevice(); - const [contacts] = useContacts(); + const [contacts, , isLoading] = useContacts(); const [searchParams] = useSearchParams(); const referal = searchParams.get('referal'); + if (isLoading) { + return
Loading...
; + } + return (
{ const navigate = useNavigate(); @@ -17,6 +17,7 @@ export const Conversation = () => { const textAreaRef = useRef(null); const device = useCurrentDevice(); const { phoneNumber } = useParams<{ phoneNumber: string }>(); + const contact = useContactPhoneNumber(phoneNumber); const messages = useConversationMessages(phoneNumber); @@ -79,7 +80,7 @@ export const Conversation = () => { return (
} + title={contact?.name || phoneNumber} left={ + + } + />
    {conversations.map((phoneNumber) => { @@ -25,6 +33,8 @@ export const MessagesApp = () => { ); })}
+ +
); }; diff --git a/src/ui/src/Providers.tsx b/src/ui/src/Providers.tsx index 01dc75320..3af55ec9a 100644 --- a/src/ui/src/Providers.tsx +++ b/src/ui/src/Providers.tsx @@ -23,7 +23,7 @@ export const queryClient = new QueryClient({ export const Providers = () => { return ( - + diff --git a/src/ui/src/api/device.ts b/src/ui/src/api/device.ts index e5da62c53..d4f7dbdeb 100644 --- a/src/ui/src/api/device.ts +++ b/src/ui/src/api/device.ts @@ -1,4 +1,4 @@ -import { Call, DeviceWithSimCard } from '../../../shared/Types'; +import { Call, CallWithPhoneNumbers, DeviceWithSimCard } from '../../../shared/Types'; import { StringifyDates } from '../../../shared/TypeUtils'; import { instance } from '../utils/fetch'; @@ -19,7 +19,10 @@ export const getMyCalls = async () => { export const getActiveCall = async () => { try { - const response = await instance.post<{ payload: StringifyDates }>('/calls/active', {}); + const response = await instance.post<{ payload: StringifyDates }>( + '/calls/active', + {}, + ); return response.data; } catch (error) { console.error('Error from getActiveCall:', error); diff --git a/src/ui/src/api/hooks/useActiveCall.ts b/src/ui/src/api/hooks/useActiveCall.ts index 09a344a5c..93acfa164 100644 --- a/src/ui/src/api/hooks/useActiveCall.ts +++ b/src/ui/src/api/hooks/useActiveCall.ts @@ -2,7 +2,7 @@ import { useQuery } from '@tanstack/react-query'; import { queryClient } from '../../Providers'; import { getActiveCall } from '../device'; import { useBroadcastEvent } from '../../hooks/useBroadcastEvent'; -import { Call } from '../../../../shared/Types'; +import { CallWithPhoneNumbers } from '../../../../shared/Types'; import { useNotifications } from '@/contexts/NotificationContext/useNotifications'; import { useCurrentDevice } from './useCurrentDevice'; @@ -27,17 +27,25 @@ export const useActiveCall = (): [ActiveCallResult, (setEmpty?: boolean) => void }); }; - useBroadcastEvent('active-call:updated', (data) => { + useBroadcastEvent('active-call:updated', (data) => { console.log('active-call:updated', data); - console.log('Setting active call to:', data); - if (!data?.ended_at && !data?.declined_at && currentDevice?.sim_card_id === data?.receiver_id) { + if (!data) { + return; + } + + if ( + !data?.ended_at && + !data?.declined_at && + !data?.accepted_at && + currentDevice?.sim_card_id === data?.receiver_id + ) { add({ type: 'call', appId: 'calls', - title: 'Incoming Call', + title: data?.caller_phone_number || 'Anonymous', path: '/apps/calls/call', - description: `Incoming call from ${data?.caller_id}`, + description: `Incoming call ..`, }); } diff --git a/src/ui/src/api/hooks/useContactPhoneNumber.ts b/src/ui/src/api/hooks/useContactPhoneNumber.ts index 0577d131e..b8962ad04 100644 --- a/src/ui/src/api/hooks/useContactPhoneNumber.ts +++ b/src/ui/src/api/hooks/useContactPhoneNumber.ts @@ -1,6 +1,6 @@ import { useContacts } from './useContacts'; -export const useContactPhoneNumber = (phoneNumber: string) => { +export const useContactPhoneNumber = (phoneNumber?: string) => { const [contacts] = useContacts(); const contact = contacts.find((contacts) => contacts.phone_number === phoneNumber); return contact; diff --git a/src/ui/src/api/hooks/useMessagesNotifications.ts b/src/ui/src/api/hooks/useMessagesNotifications.ts new file mode 100644 index 000000000..7427ddcb0 --- /dev/null +++ b/src/ui/src/api/hooks/useMessagesNotifications.ts @@ -0,0 +1,26 @@ +import { useNotifications } from '@/contexts/NotificationContext/useNotifications'; +import { useBroadcastEvent } from '@/hooks/useBroadcastEvent'; +import { MessageWithPhoneNumbers } from '../../../../shared/Types'; +import { useCurrentDevice } from './useCurrentDevice'; + +export const useMessagesNotifications = () => { + const currentDevice = useCurrentDevice(); + const { add } = useNotifications(); + + useBroadcastEvent('message:new-message', (data) => { + if (!data || !currentDevice) { + return; + } + + if (data.receiver_id === currentDevice?.sim_card_id) { + add({ + title: data.sender_phone_number, + path: `/apps/conversation/${data.sender_phone_number}`, + description: data.content, + type: 'generic', + appId: 'messages', + timeout: 5000, + }); + } + }); +}; diff --git a/src/ui/src/components/Forms/NewMessageForm.tsx b/src/ui/src/components/Forms/NewMessageForm.tsx new file mode 100644 index 000000000..0821d48a9 --- /dev/null +++ b/src/ui/src/components/Forms/NewMessageForm.tsx @@ -0,0 +1,76 @@ +import { useRef } from 'react'; +import { Send } from 'react-feather'; +import { instance } from '@/utils/fetch'; +import { queryClient } from '@/Providers'; +import { Input } from '../Input'; + +export interface NewMessageFormProps { + phoneNumber?: string; + onMessageSent: () => void; +} + +export const NewMessageForm = ({ + phoneNumber: initialPhoneNumber, + onMessageSent, +}: NewMessageFormProps) => { + const formRef = useRef(null); + const textAreaRef = useRef(null); + + const onSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + const form = e.currentTarget as HTMLFormElement; + const message = form.querySelector('textarea')?.value; + const phoneNumber = initialPhoneNumber || form.querySelector('input')?.value; + + if (!phoneNumber || !message) { + return; + } + + // Send message to phoneNumber + await instance.post('/messages/send', { + content: message, + phoneNumber, + }); + + onMessageSent(); + + textAreaRef.current?.focus(); + + form.reset(); + queryClient.invalidateQueries({ + queryKey: ['messages'], + }); + queryClient.invalidateQueries({ + queryKey: ['conversations'], + }); + queryClient.invalidateQueries({ + queryKey: ['conversation', phoneNumber], + }); + }; + + return ( + + {!initialPhoneNumber && } + +