diff --git a/MiniSpace.APIGateway/src/MiniSpace.APIGateway/ntrada.yml b/MiniSpace.APIGateway/src/MiniSpace.APIGateway/ntrada.yml index 46a179f44..dae0f3140 100644 --- a/MiniSpace.APIGateway/src/MiniSpace.APIGateway/ntrada.yml +++ b/MiniSpace.APIGateway/src/MiniSpace.APIGateway/ntrada.yml @@ -302,6 +302,88 @@ modules: notifications-service: localUrl: localhost:5006 url: notifications-service + + + communication: + path: /communication + routes: + - upstream: /chats + method: POST + use: downstream + downstream: communication-service/communication/chats + auth: true + + - upstream: /chats/{chatId}/messages + method: POST + use: downstream + downstream: communication-service/communication/chats/{chatId}/messages + auth: true + bind: + - chatId:{chatId} + + - upstream: /chats/{chatId}/users + method: PUT + use: downstream + downstream: communication-service/communication/chats/{chatId}/users + auth: true + bind: + - chatId:{chatId} + + - upstream: /chats/{chatId}/messages/{messageId}/status + method: PUT + use: downstream + downstream: communication-service/communication/chats/{chatId}/messages/{messageId}/status + auth: true + bind: + - chatId:{chatId} + - messageId:{messageId} + + - upstream: /chats/user/{userId} + method: GET + use: downstream + downstream: communication-service/communication/chats/user/{userId} + auth: true + bind: + - userId:{userId} + + - upstream: /chats/{chatId} + method: GET + use: downstream + downstream: communication-service/communication/chats/{chatId} + auth: true + bind: + - chatId:{chatId} + + - upstream: /chats/{chatId}/messages + method: GET + use: downstream + downstream: communication-service/communication/chats/{chatId}/messages + auth: true + bind: + - chatId:{chatId} + + - upstream: /chats/{chatId}/{userId} + method: DELETE + use: downstream + downstream: communication-service/communication/chats/{chatId}/{userId} + auth: true + bind: + - chatId:{chatId} + - userId:{userId} + + - upstream: /chats/{chatId}/messages/{messageId} + method: DELETE + use: downstream + downstream: communication-service/communication/chats/{chatId}/messages/{messageId} + auth: true + bind: + - chatId:{chatId} + - messageId:{messageId} + + services: + communication-service: + localUrl: localhost:5016 + url: communication-service students: @@ -349,6 +431,30 @@ modules: downstream: students-service/students/{studentId}/notifications auth: true + - upstream: /profiles/users/{userId}/views/paginated + method: GET + use: downstream + downstream: students-service/students/profiles/users/{userId}/views/paginated + auth: true + bind: + - userId:{userId} + + - upstream: /profiles/users/{userId}/views/viewed + method: GET + use: downstream + downstream: students-service/students/profiles/users/{userId}/views/viewed + auth: true + bind: + - userId:{userId} + + - upstream: /{blockerId}/blocked-users + method: GET + use: downstream + downstream: students-service/students/{blockerId}/blocked-users + auth: true + bind: + - blockerId:{blockerId} + - upstream: /{studentId} method: PUT use: downstream @@ -392,6 +498,32 @@ modules: downstream: students-service/students/{studentId}/notifications auth: true + - upstream: /profiles/users/{userProfileId}/view + method: POST + use: downstream + downstream: students-service/students/profiles/users/{userProfileId}/view + auth: true + bind: + - userProfileId:{userProfileId} + + - upstream: /{blockerId}/block-user/{blockedUserId} + method: POST + use: downstream + downstream: students-service/students/{blockerId}/block-user/{blockedUserId} + auth: true + bind: + - blockerId:{blockerId} + - blockedUserId:{blockedUserId} + + - upstream: /{blockerId}/unblock-user/{blockedUserId} + method: POST + use: downstream + downstream: students-service/students/{blockerId}/unblock-user/{blockedUserId} + auth: true + bind: + - blockerId:{blockerId} + - blockedUserId:{blockedUserId} + - upstream: /{studentId}/languages-and-interests method: PUT use: downstream @@ -454,6 +586,23 @@ modules: downstream: events-service/events/student/{studentId} auth: true + - upstream: /users/{userId}/feed + method: GET + use: downstream + downstream: events-service/events/users/{userId}/feed + auth: true + bind: + - userId:{userId} + + - upstream: /users/{userId}/views/paginated + method: GET + use: downstream + downstream: events-service/events/users/{userId}/views/paginated + auth: true + bind: + - userId:{userId} + + - upstream: /{eventId} method: DELETE use: downstream @@ -483,6 +632,14 @@ modules: downstream: events-service/events/{eventId}/show-interest auth: true + - upstream: /{eventId}/view + method: POST + use: downstream + downstream: events-service/events/{eventId}/view + auth: true + bind: + - eventId:{eventId} + - upstream: /{eventId}/show-interest method: DELETE use: downstream @@ -757,6 +914,22 @@ modules: downstream: friends-service/friends/requests/sent/{userId} auth: true + - upstream: /{userId}/followers + method: GET + use: downstream + downstream: friends-service/friends/{userId}/followers + auth: true + bind: + - userId: {userId} + + - upstream: /{userId}/following + method: GET + use: downstream + downstream: friends-service/friends/{userId}/following + auth: true + bind: + - userId: {userId} + - upstream: /requests/{userId}/withdraw method: PUT use: downstream @@ -918,7 +1091,7 @@ modules: - upstream: /users/{userId}/organizations method: GET use: downstream - downstream: organizations-service/users/{userId}/organizations + downstream: organizations-service/organizations/users/{userId}/organizations auth: true bind: - userId: {userId} @@ -930,6 +1103,16 @@ modules: auth: true bind: - organizationId: {organizationId} + + - upstream: /users/{userId}/organizations/follow + method: GET + use: downstream + downstream: organizations-service/organizations/users/{userId}/organizations/follow + auth: true + bind: + - userId: {userId} + + - upstream: /{organizationId}/details/gallery-users method: GET @@ -946,6 +1129,13 @@ modules: bind: - organizationId: {organizationId} + - upstream: /{organizationId}/requests + method: GET + use: downstream + downstream: organizations-service/organizations/{organizationId}/requests + bind: + - organizationId: {organizationId} + - upstream: /paginated method: GET use: downstream diff --git a/MiniSpace.Services.Communication/.gitignore b/MiniSpace.Services.Communication/.gitignore new file mode 100644 index 000000000..c619427ad --- /dev/null +++ b/MiniSpace.Services.Communication/.gitignore @@ -0,0 +1,338 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ +# **/Properties/launchSettings.json + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +bin/ +obj/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush +.cr/ + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +logs/ + + +# Ignore appsettings.json files +**/appsettings*.json \ No newline at end of file diff --git a/MiniSpace.Services.Communication/Dockerfile b/MiniSpace.Services.Communication/Dockerfile new file mode 100644 index 000000000..88978fc22 --- /dev/null +++ b/MiniSpace.Services.Communication/Dockerfile @@ -0,0 +1,17 @@ +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +WORKDIR /app + +COPY . . + +RUN dotnet publish src/MiniSpace.Services.Communication.Api -c Release -o out + +FROM mcr.microsoft.com/dotnet/aspnet:8.0 +WORKDIR /app + +COPY --from=build /app/out . + +ENV ASPNETCORE_URLS=http://*:80 +ENV ASPNETCORE_ENVIRONMENT=docker +ENV NTRADA_CONFIG=ntrada.docker + +ENTRYPOINT ["dotnet", "MiniSpace.Services.Communication.Api.dll"] \ No newline at end of file diff --git a/MiniSpace.Services.Communication/LICENSE b/MiniSpace.Services.Communication/LICENSE new file mode 100644 index 000000000..c3a0bdfb9 --- /dev/null +++ b/MiniSpace.Services.Communication/LICENSE @@ -0,0 +1,89 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +## TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +### 1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, that is made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +### 2. Grant of Copyright License. + +Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +### 3. Grant of Patent License. + +Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +### 4. Redistribution. + +You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + +(a) You must give any other recipients of the Work or Derivative Works a copy of this License; and + +(b) You must cause any modified files to carry prominent notices stating that You changed the files; and + +(c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + +(d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + +You may add Your own copyright notices to Your version of this material, provided that such additional copyright notices are not considered to be modifications of the License. + +### 5. Submission of Contributions. + +Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +### 6. Trademarks. + +This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +### 7. Disclaimer of Warranty. + +Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +### 8. Limitation of Liability. + +In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +### 9. Accepting Warranty or Additional Liability. + +While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, protection, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +## END OF TERMS AND CONDITIONS + +### Appendix: How to apply the Apache License to your work. + +To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be placed in the LICENSE file in the root directory of your source code (if not already present): + + Copyright 2024 distributed_minispace_team + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/MiniSpace.Services.Communication/MiniSpace.Services.Communication.sln b/MiniSpace.Services.Communication/MiniSpace.Services.Communication.sln new file mode 100644 index 000000000..ff5faab9b --- /dev/null +++ b/MiniSpace.Services.Communication/MiniSpace.Services.Communication.sln @@ -0,0 +1,48 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{0E976732-0CD9-4D2E-B989-998B124073BA}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MiniSpace.Services.Students.Api", "src\MiniSpace.Services.Students.Api\MiniSpace.Services.Students.Api.csproj", "{D915BFB5-D2D4-44C8-A3A3-379419079F06}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MiniSpace.Services.Students.Application", "src\MiniSpace.Services.Students.Application\MiniSpace.Services.Students.Application.csproj", "{97B39658-9B33-4124-90E5-102FDA7D3733}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MiniSpace.Services.Students.Core", "src\MiniSpace.Services.Students.Core\MiniSpace.Services.Students.Core.csproj", "{C4EFD7FD-9D95-444B-86A4-E4D60E62CD16}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MiniSpace.Services.Students.Infrastructure", "src\MiniSpace.Services.Students.Infrastructure\MiniSpace.Services.Students.Infrastructure.csproj", "{B2997BE8-0CE3-45DD-98E9-80599B070C25}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {D915BFB5-D2D4-44C8-A3A3-379419079F06}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D915BFB5-D2D4-44C8-A3A3-379419079F06}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D915BFB5-D2D4-44C8-A3A3-379419079F06}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D915BFB5-D2D4-44C8-A3A3-379419079F06}.Release|Any CPU.Build.0 = Release|Any CPU + {97B39658-9B33-4124-90E5-102FDA7D3733}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {97B39658-9B33-4124-90E5-102FDA7D3733}.Debug|Any CPU.Build.0 = Debug|Any CPU + {97B39658-9B33-4124-90E5-102FDA7D3733}.Release|Any CPU.ActiveCfg = Release|Any CPU + {97B39658-9B33-4124-90E5-102FDA7D3733}.Release|Any CPU.Build.0 = Release|Any CPU + {C4EFD7FD-9D95-444B-86A4-E4D60E62CD16}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C4EFD7FD-9D95-444B-86A4-E4D60E62CD16}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C4EFD7FD-9D95-444B-86A4-E4D60E62CD16}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C4EFD7FD-9D95-444B-86A4-E4D60E62CD16}.Release|Any CPU.Build.0 = Release|Any CPU + {B2997BE8-0CE3-45DD-98E9-80599B070C25}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B2997BE8-0CE3-45DD-98E9-80599B070C25}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B2997BE8-0CE3-45DD-98E9-80599B070C25}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B2997BE8-0CE3-45DD-98E9-80599B070C25}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {D915BFB5-D2D4-44C8-A3A3-379419079F06} = {0E976732-0CD9-4D2E-B989-998B124073BA} + {97B39658-9B33-4124-90E5-102FDA7D3733} = {0E976732-0CD9-4D2E-B989-998B124073BA} + {C4EFD7FD-9D95-444B-86A4-E4D60E62CD16} = {0E976732-0CD9-4D2E-B989-998B124073BA} + {B2997BE8-0CE3-45DD-98E9-80599B070C25} = {0E976732-0CD9-4D2E-B989-998B124073BA} + EndGlobalSection +EndGlobal diff --git a/MiniSpace.Services.Communication/scripts/build.sh b/MiniSpace.Services.Communication/scripts/build.sh new file mode 100644 index 000000000..3affad0eb --- /dev/null +++ b/MiniSpace.Services.Communication/scripts/build.sh @@ -0,0 +1,2 @@ +#!/bin/bash +dotnet build -c release \ No newline at end of file diff --git a/MiniSpace.Services.Communication/scripts/dockerize-tag-push.sh b/MiniSpace.Services.Communication/scripts/dockerize-tag-push.sh new file mode 100755 index 000000000..018a7d1c1 --- /dev/null +++ b/MiniSpace.Services.Communication/scripts/dockerize-tag-push.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +export ASPNETCORE_ENVIRONMENT=docker + +cd .. + +docker build -t minispace.services.communication:latest . + +docker tag minispace.services.communication:latest adrianvsaint/minispace.services.communication:latest + +docker push adrianvsaint/minispace.services.communication:latest diff --git a/MiniSpace.Services.Communication/scripts/start.sh b/MiniSpace.Services.Communication/scripts/start.sh new file mode 100644 index 000000000..824533d3b --- /dev/null +++ b/MiniSpace.Services.Communication/scripts/start.sh @@ -0,0 +1,4 @@ +#!/bin/bash +export ASPNETCORE_ENVIRONMENT=local +cd ../src/MiniSpace.Services.Communication.Api +dotnet run \ No newline at end of file diff --git a/MiniSpace.Services.Communication/scripts/test.sh b/MiniSpace.Services.Communication/scripts/test.sh new file mode 100644 index 000000000..6046c35a0 --- /dev/null +++ b/MiniSpace.Services.Communication/scripts/test.sh @@ -0,0 +1,2 @@ +#!/bin/bash +dotnet test \ No newline at end of file diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Api/.gitignore b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Api/.gitignore new file mode 100644 index 000000000..94196c0d8 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Api/.gitignore @@ -0,0 +1,339 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +appsettings.local.json +appsettings.docker.json +appsettings.json + + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ +# **/Properties/launchSettings.json + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +bin/ +obj/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush +.cr/ + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +logs/ diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Api/MiniSpace.Services.Communication.Api.csproj b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Api/MiniSpace.Services.Communication.Api.csproj new file mode 100644 index 000000000..7219ce31d --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Api/MiniSpace.Services.Communication.Api.csproj @@ -0,0 +1,20 @@ + + + + net8.0 + latest + MiniSpace.Services.Students.Api + + + + + + + + + + + + + + diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Api/MiniSpace.Services.Communication.Api.sln b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Api/MiniSpace.Services.Communication.Api.sln new file mode 100644 index 000000000..7eb3ebc70 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Api/MiniSpace.Services.Communication.Api.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.002.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MiniSpace.Services.Communication.Api", "MiniSpace.Services.Communication.Api.csproj", "{5DA6DF02-FFC5-4BAB-B1FC-739A9DA14602}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {5DA6DF02-FFC5-4BAB-B1FC-739A9DA14602}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5DA6DF02-FFC5-4BAB-B1FC-739A9DA14602}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5DA6DF02-FFC5-4BAB-B1FC-739A9DA14602}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5DA6DF02-FFC5-4BAB-B1FC-739A9DA14602}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {284B46A7-2ABB-4E20-8B52-4291CC46AE00} + EndGlobalSection +EndGlobal diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Api/Program.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Api/Program.cs new file mode 100644 index 000000000..601b5b58d --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Api/Program.cs @@ -0,0 +1,94 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Convey; +using Convey.Logging; +using Convey.Types; +using Convey.WebApi; +using Convey.WebApi.CQRS; +using Microsoft.AspNetCore; +using Microsoft.AspNetCore.SignalR; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using MiniSpace.Services.Communication.Application; +using MiniSpace.Services.Communication.Application.Services; +using MiniSpace.Services.Communication.Application.Commands; +using MiniSpace.Services.Communication.Application.Dto; +using MiniSpace.Services.Communication.Application.Queries; +using MiniSpace.Services.Communication.Infrastructure; +using MiniSpace.Services.Communication.Application.Hubs; +using MiniSpace.Services.Communication.Core.Wrappers; +using System; + +namespace MiniSpace.Services.Communication.Api +{ + public class Program + { + public static async Task Main(string[] args) + => await WebHost.CreateDefaultBuilder(args) + .ConfigureServices(services => + { + services.AddConvey() + .AddWebApi() + .AddApplication() + .AddInfrastructure(); + services.AddCors(options => + { + options.AddPolicy("CorsPolicy", + builder => builder + .AllowAnyMethod() + .AllowAnyHeader() + .AllowCredentials() + .SetIsOriginAllowed((host) => true)); + }); + services.AddSignalR(); + services.AddAuthentication(); + services.AddAuthorization(); + }) + .Configure(app => app + .UseInfrastructure() + .UseRouting() + .UseCors("CorsPolicy") + .UseAuthentication() + .UseAuthorization() + .UseEndpoints(endpoints => + { + endpoints.MapHub("/chatHub").RequireCors("CorsPolicy"); + }) + .UseDispatcherEndpoints(endpoints => endpoints + + // Chat-related endpoints + .Get>("communication/chats/user/{userId}") + .Get("communication/chats/{chatId}") + .Get>("communication/chats/{chatId}/messages") + .Post("communication/chats", async (cmd, ctx) => + { + try + { + var chatId = await ctx.RequestServices.GetService() + .CreateChatAsync(cmd.ChatId, cmd.ParticipantIds, cmd.ChatName); + ctx.Response.StatusCode = StatusCodes.Status200OK; + await ctx.Response.WriteJsonAsync(new { ChatId = chatId }); + } + catch (Exception ex) + { + ctx.Response.StatusCode = StatusCodes.Status500InternalServerError; + await ctx.Response.WriteJsonAsync(new { Error = ex.Message }); + } + }) + + .Put("communication/chats/{chatId}/users") + .Delete("communication/chats/{chatId}/{userId}") + + // Message-related endpoints + .Post("communication/chats/{chatId}/messages") + .Put("communication/chats/{chatId}/messages/{messageId}/status") + .Delete("communication/chats/{chatId}/messages/{messageId}") + )) + .UseLogging() + .UseLogging() + .Build() + .RunAsync(); + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Api/Properties/launchSettings.json b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Api/Properties/launchSettings.json new file mode 100644 index 000000000..41c0e71a3 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Api/Properties/launchSettings.json @@ -0,0 +1,26 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:5016" + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": false, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "local" + } + }, + "MiniSpace.Services.Communication": { + "commandName": "Project", + "launchBrowser": false, + "applicationUrl": "http://localhost:5016", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "local" + } + } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/.gitignore b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/.gitignore new file mode 100644 index 000000000..64def56a6 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/.gitignore @@ -0,0 +1,331 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ +# **/Properties/launchSettings.json + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush +.cr/ + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +logs/ diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Commands/AddUserToChat.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Commands/AddUserToChat.cs new file mode 100644 index 000000000..b9c8b36bf --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Commands/AddUserToChat.cs @@ -0,0 +1,17 @@ +using Convey.CQRS.Commands; +using System; + +namespace MiniSpace.Services.Communication.Application.Commands +{ + public class AddUserToChat : ICommand + { + public Guid ChatId { get; } + public Guid UserId { get; } + + public AddUserToChat(Guid chatId, Guid userId) + { + ChatId = chatId; + UserId = userId; + } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Commands/CreateChat.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Commands/CreateChat.cs new file mode 100644 index 000000000..8a81dfd02 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Commands/CreateChat.cs @@ -0,0 +1,20 @@ +using Convey.CQRS.Commands; +using System; +using System.Collections.Generic; + +namespace MiniSpace.Services.Communication.Application.Commands +{ + public class CreateChat : ICommand + { + public Guid ChatId { get; } + public List ParticipantIds { get; } + public string ChatName { get; } + + public CreateChat(Guid chatId, List participantIds, string chatName = null) + { + ChatId = chatId; + ParticipantIds = participantIds ?? new List(); + ChatName = chatName; + } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Commands/DeleteChat.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Commands/DeleteChat.cs new file mode 100644 index 000000000..f1cf27b57 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Commands/DeleteChat.cs @@ -0,0 +1,19 @@ +using Convey.CQRS.Commands; +using Microsoft.AspNetCore.Mvc; +using System; + +namespace MiniSpace.Services.Communication.Application.Commands +{ + public class DeleteChat : ICommand + { + public Guid ChatId { get; set; } + + public Guid UserId { get; set; } + + public DeleteChat(Guid chatId, Guid userId) + { + ChatId = chatId; + UserId = userId; + } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Commands/DeleteMessage.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Commands/DeleteMessage.cs new file mode 100644 index 000000000..b6fc99d17 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Commands/DeleteMessage.cs @@ -0,0 +1,21 @@ +using Convey.CQRS.Commands; +using Microsoft.AspNetCore.Mvc; +using System; + +namespace MiniSpace.Services.Communication.Application.Commands +{ + public class DeleteMessage : ICommand + { + [FromRoute] + public Guid MessageId { get; } + + [FromQuery] + public Guid ChatId { get; } + + public DeleteMessage(Guid messageId, Guid chatId) + { + MessageId = messageId; + ChatId = chatId; + } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Commands/Handlers/AddUserToChatHandler.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Commands/Handlers/AddUserToChatHandler.cs new file mode 100644 index 000000000..9a0bbd1be --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Commands/Handlers/AddUserToChatHandler.cs @@ -0,0 +1,38 @@ +using Convey.CQRS.Commands; +using MiniSpace.Services.Communication.Application.Commands; +using MiniSpace.Services.Communication.Application.Events; +using MiniSpace.Services.Communication.Application.Services; +using MiniSpace.Services.Communication.Core.Entities; +using MiniSpace.Services.Communication.Core.Repositories; +using System.Threading; +using System.Threading.Tasks; + +namespace MiniSpace.Services.Communication.Application.Commands.Handlers +{ + public class AddUserToChatHandler : ICommandHandler + { + private readonly IUserChatsRepository _userChatsRepository; + private readonly IMessageBroker _messageBroker; + + public AddUserToChatHandler(IUserChatsRepository userChatsRepository, IMessageBroker messageBroker) + { + _userChatsRepository = userChatsRepository; + _messageBroker = messageBroker; + } + + public async Task HandleAsync(AddUserToChat command, CancellationToken cancellationToken) + { + var userChats = await _userChatsRepository.GetByUserIdAsync(command.UserId) ?? new UserChats(command.UserId); + var chat = userChats.GetChatById(command.ChatId); + + if (chat == null) + { + chat = new Chat(new List { command.UserId }); + userChats.AddChat(chat); + await _userChatsRepository.AddOrUpdateAsync(userChats); + } + + await _messageBroker.PublishAsync(new UserAddedToChat(command.ChatId, command.UserId)); + } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Commands/Handlers/CreateChatHandler.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Commands/Handlers/CreateChatHandler.cs new file mode 100644 index 000000000..121c5316a --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Commands/Handlers/CreateChatHandler.cs @@ -0,0 +1,31 @@ +using Convey.CQRS.Commands; +using Microsoft.Extensions.Logging; +using MiniSpace.Services.Communication.Application.Commands; +using MiniSpace.Services.Communication.Application.Services; +using System.Threading; +using System.Threading.Tasks; + +namespace MiniSpace.Services.Communication.Application.Commands.Handlers +{ + public class CreateChatHandler : ICommandHandler + { + private readonly ICommunicationService _communicationService; + private readonly ILogger _logger; + + public CreateChatHandler(ICommunicationService communicationService, ILogger logger) + { + _communicationService = communicationService; + _logger = logger; + } + + public async Task HandleAsync(CreateChat command, CancellationToken cancellationToken) + { + _logger.LogInformation($"Handling CreateChat command for Chat ID: {command.ChatId}"); + + // Call the CommunicationService to create the chat + var chatId = await _communicationService.CreateChatAsync(command.ChatId, command.ParticipantIds, command.ChatName); + + _logger.LogInformation($"Chat created with ID: {chatId}"); + } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Commands/Handlers/DeleteChatHandler.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Commands/Handlers/DeleteChatHandler.cs new file mode 100644 index 000000000..3622fc5f5 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Commands/Handlers/DeleteChatHandler.cs @@ -0,0 +1,31 @@ +using Convey.CQRS.Commands; +using MiniSpace.Services.Communication.Application.Commands; +using MiniSpace.Services.Communication.Application.Events; +using MiniSpace.Services.Communication.Application.Services; +using MiniSpace.Services.Communication.Core.Repositories; +using System.Threading; +using System.Threading.Tasks; + +namespace MiniSpace.Services.Communication.Application.Commands.Handlers +{ + public class DeleteChatHandler : ICommandHandler + { + private readonly IUserChatsRepository _userChatsRepository; + private readonly IMessageBroker _messageBroker; + + public DeleteChatHandler(IUserChatsRepository userChatsRepository, IMessageBroker messageBroker) + { + _userChatsRepository = userChatsRepository; + _messageBroker = messageBroker; + } + + public async Task HandleAsync(DeleteChat command, CancellationToken cancellationToken) + { + Console.WriteLine($"Received DeleteChat command - ChatId: {command.ChatId}, UserId: {command.UserId}"); + + await _userChatsRepository.DeleteChatAsync(command.UserId, command.ChatId); + + await _messageBroker.PublishAsync(new ChatDeleted(command.ChatId)); + } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Commands/Handlers/DeleteMessageHandler.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Commands/Handlers/DeleteMessageHandler.cs new file mode 100644 index 000000000..daaf6af7b --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Commands/Handlers/DeleteMessageHandler.cs @@ -0,0 +1,40 @@ +using Convey.CQRS.Commands; +using MiniSpace.Services.Communication.Application.Commands; +using MiniSpace.Services.Communication.Application.Services; +using MiniSpace.Services.Communication.Core.Repositories; +using MiniSpace.Services.Communication.Application.Events; +using System.Threading; +using System.Threading.Tasks; + +namespace MiniSpace.Services.Communication.Application.Commands.Handlers +{ + public class DeleteMessageHandler : ICommandHandler + { + private readonly IUserChatsRepository _userChatsRepository; + private readonly IMessageBroker _messageBroker; + + public DeleteMessageHandler(IUserChatsRepository userChatsRepository, IMessageBroker messageBroker) + { + _userChatsRepository = userChatsRepository; + _messageBroker = messageBroker; + } + + public async Task HandleAsync(DeleteMessage command, CancellationToken cancellationToken) + { + var userChats = await _userChatsRepository.GetByUserIdAsync(command.ChatId); + var chat = userChats?.GetChatById(command.ChatId); + + if (chat != null) + { + var message = chat.Messages.Find(m => m.Id == command.MessageId); + if (message != null) + { + chat.Messages.Remove(message); + await _userChatsRepository.UpdateAsync(userChats); + + await _messageBroker.PublishAsync(new MessageStatusUpdated(command.ChatId, command.MessageId, "Deleted")); + } + } + } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Commands/Handlers/RemoveUserFromChatHandler.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Commands/Handlers/RemoveUserFromChatHandler.cs new file mode 100644 index 000000000..5a7291c36 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Commands/Handlers/RemoveUserFromChatHandler.cs @@ -0,0 +1,36 @@ +using Convey.CQRS.Commands; +using MiniSpace.Services.Communication.Application.Commands; +using MiniSpace.Services.Communication.Application.Events; +using MiniSpace.Services.Communication.Application.Services; +using MiniSpace.Services.Communication.Core.Repositories; +using System.Threading; +using System.Threading.Tasks; + +namespace MiniSpace.Services.Communication.Application.Commands.Handlers +{ + public class RemoveUserFromChatHandler : ICommandHandler + { + private readonly IUserChatsRepository _userChatsRepository; + private readonly IMessageBroker _messageBroker; + + public RemoveUserFromChatHandler(IUserChatsRepository userChatsRepository, IMessageBroker messageBroker) + { + _userChatsRepository = userChatsRepository; + _messageBroker = messageBroker; + } + + public async Task HandleAsync(RemoveUserFromChat command, CancellationToken cancellationToken) + { + var userChats = await _userChatsRepository.GetByUserIdAsync(command.UserId); + var chat = userChats?.GetChatById(command.ChatId); + + if (chat != null) + { + chat.ParticipantIds.Remove(command.UserId); + await _userChatsRepository.UpdateAsync(userChats); + + await _messageBroker.PublishAsync(new UserAddedToChat(command.ChatId, command.UserId)); + } + } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Commands/Handlers/SendMessageHandler.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Commands/Handlers/SendMessageHandler.cs new file mode 100644 index 000000000..c273375f6 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Commands/Handlers/SendMessageHandler.cs @@ -0,0 +1,152 @@ +using Convey.CQRS.Commands; +using Microsoft.Extensions.Logging; +using Microsoft.AspNetCore.SignalR; +using MiniSpace.Services.Communication.Application.Commands; +using MiniSpace.Services.Communication.Application.Events; +using MiniSpace.Services.Communication.Application.Hubs; +using MiniSpace.Services.Communication.Application.Services; +using MiniSpace.Services.Communication.Core.Entities; +using MiniSpace.Services.Communication.Core.Repositories; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using System; +using MiniSpace.Services.Communication.Application.Dto; +using System.Collections.Generic; + +namespace MiniSpace.Services.Communication.Application.Commands.Handlers +{ + public class SendMessageHandler : ICommandHandler + { + private readonly IUserChatsRepository _userChatsRepository; + private readonly IMessageBroker _messageBroker; + private readonly ILogger _logger; + private readonly IHubContext _hubContext; + + public SendMessageHandler( + IUserChatsRepository userChatsRepository, + IMessageBroker messageBroker, + ILogger logger, + IHubContext hubContext) + { + _userChatsRepository = userChatsRepository; + _messageBroker = messageBroker; + _logger = logger; + _hubContext = hubContext; + } + + public async Task HandleAsync(SendMessage command, CancellationToken cancellationToken) + { + // Retrieve the chat from the sender's chat list + var senderChats = await _userChatsRepository.GetByUserIdAsync(command.SenderId); + var chat = senderChats?.GetChatById(command.ChatId); + + if (chat == null) + { + _logger.LogWarning($"Chat with id {command.ChatId} not found for user with id {command.SenderId}. Checking if it exists for other participants..."); + + // If the chat is not found, check if it exists for any other participant + chat = await FindChatInAllParticipants(command.SenderId, command.ChatId); + + if (chat == null) + { + _logger.LogInformation($"No existing chat found. Creating a new chat for the participants."); + // If no chat is found at all, create a new one + // If creating a new chat + chat = new Chat(command.ChatId, new List { command.SenderId }, new List()); + + await CreateOrUpdateChatForAllParticipants(chat); + } + else + { + // Restore the chat for the sender + senderChats ??= new UserChats(command.SenderId); + senderChats.AddChat(chat); + await _userChatsRepository.AddOrUpdateAsync(senderChats); + } + } + + // Create the message to be sent + var message = new Message( + chatId: chat.Id, + senderId: command.SenderId, + receiverId: Guid.Empty, // Unused in this context + content: command.Content, + type: Enum.Parse(command.MessageType) + ); + + _logger.LogInformation($"Sending message with id {message.Id} to chat with id {chat.Id}"); + + chat.AddMessage(message); + + // Save the updated chat and message to the database + await UpdateChatForAllParticipants(chat); + + // Use the message ID from the database for the MessageDto + var messageDto = new MessageDto + { + Id = message.Id, // Use the ID from the database + ChatId = chat.Id, + SenderId = command.SenderId, + Content = command.Content, + Timestamp = message.Timestamp // Assuming Message has a Timestamp property + }; + + + // Notify the participant via SignalR + await ChatHub.SendMessageToUser(_hubContext, command.SenderId.ToString(), messageDto, _logger); + + // Publish the MessageSent event for further processing + await _messageBroker.PublishAsync(new MessageSent( + chatId: chat.Id, + messageId: message.Id, + senderId: command.SenderId, + content: command.Content + )); + } + + private async Task FindChatInAllParticipants(Guid senderId, Guid chatId) + { + // Go through all participant chats to find if the chat already exists + var participantIds = await _userChatsRepository.GetParticipantIdsByChatIdAsync(chatId); + + foreach (var participantId in participantIds) + { + if (participantId == senderId) continue; + + var participantChats = await _userChatsRepository.GetByUserIdAsync(participantId); + var existingChat = participantChats?.GetChatById(chatId); + + if (existingChat != null) + { + _logger.LogInformation($"Chat found for another participant with id {participantId}. Restoring chat for sender."); + return existingChat; + } + } + + return null; + } + + private async Task CreateOrUpdateChatForAllParticipants(Chat chat) + { + foreach (var participantId in chat.ParticipantIds) + { + var participantChats = await _userChatsRepository.GetByUserIdAsync(participantId) ?? new UserChats(participantId); + participantChats.AddChat(chat); + await _userChatsRepository.AddOrUpdateAsync(participantChats); + } + } + + private async Task UpdateChatForAllParticipants(Chat chat) + { + foreach (var participantId in chat.ParticipantIds) + { + var participantChats = await _userChatsRepository.GetByUserIdAsync(participantId) ?? new UserChats(participantId); + var participantChat = participantChats.GetChatById(chat.Id) ?? chat; + participantChat.AddMessage(chat.Messages.Last()); + await _userChatsRepository.AddOrUpdateAsync(participantChats); + } + } + + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Commands/Handlers/UpdateMessageStatusHandler.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Commands/Handlers/UpdateMessageStatusHandler.cs new file mode 100644 index 000000000..c18402d17 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Commands/Handlers/UpdateMessageStatusHandler.cs @@ -0,0 +1,36 @@ +using Convey.CQRS.Commands; +using Microsoft.Extensions.Logging; +using Microsoft.AspNetCore.SignalR; +using MiniSpace.Services.Communication.Application.Commands; +using MiniSpace.Services.Communication.Application.Events; +using MiniSpace.Services.Communication.Application.Hubs; +using MiniSpace.Services.Communication.Application.Services; +using System.Threading; +using System.Threading.Tasks; + +namespace MiniSpace.Services.Communication.Application.Commands.Handlers +{ + public class UpdateMessageStatusHandler : ICommandHandler + { + private readonly ICommunicationService _communicationService; + private readonly IHubContext _hubContext; + private readonly ILogger _logger; + + public UpdateMessageStatusHandler( + ICommunicationService communicationService, + IHubContext hubContext, + ILogger logger) + { + _communicationService = communicationService; + _hubContext = hubContext; + _logger = logger; + } + + public async Task HandleAsync(UpdateMessageStatus command, CancellationToken cancellationToken) + { + await _communicationService.UpdateMessageStatusAsync(command.ChatId, command.MessageId, command.Status); + _logger.LogInformation($"Message status updated: ChatId={command.ChatId}, MessageId={command.MessageId}, Status={command.Status}"); + await ChatHub.SendMessageStatusUpdate(_hubContext, command.ChatId.ToString(), command.MessageId, command.Status, _logger); + } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Commands/RemoveUserFromChat.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Commands/RemoveUserFromChat.cs new file mode 100644 index 000000000..53de89eaa --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Commands/RemoveUserFromChat.cs @@ -0,0 +1,17 @@ +using Convey.CQRS.Commands; +using System; + +namespace MiniSpace.Services.Communication.Application.Commands +{ + public class RemoveUserFromChat : ICommand + { + public Guid ChatId { get; } + public Guid UserId { get; } + + public RemoveUserFromChat(Guid chatId, Guid userId) + { + ChatId = chatId; + UserId = userId; + } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Commands/SendMessage.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Commands/SendMessage.cs new file mode 100644 index 000000000..a5df08bbb --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Commands/SendMessage.cs @@ -0,0 +1,21 @@ +using Convey.CQRS.Commands; +using System; + +namespace MiniSpace.Services.Communication.Application.Commands +{ + public class SendMessage : ICommand + { + public Guid ChatId { get; } + public Guid SenderId { get; } + public string Content { get; } + public string MessageType { get; } + + public SendMessage(Guid chatId, Guid senderId, string content, string messageType = "Text") + { + ChatId = chatId; + SenderId = senderId; + Content = content; + MessageType = messageType; + } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Commands/UpdateMessageStatus.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Commands/UpdateMessageStatus.cs new file mode 100644 index 000000000..cf809704e --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Commands/UpdateMessageStatus.cs @@ -0,0 +1,19 @@ +using Convey.CQRS.Commands; +using System; + +namespace MiniSpace.Services.Communication.Application.Commands +{ + public class UpdateMessageStatus : ICommand + { + public Guid ChatId { get; set; } + public Guid MessageId { get; set; } + public string Status { get; set; } + + public UpdateMessageStatus(Guid chatId, Guid messageId, string status) + { + ChatId = chatId; + MessageId = messageId; + Status = status; + } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/ContractAttribute.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/ContractAttribute.cs new file mode 100644 index 000000000..19f02dfbf --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/ContractAttribute.cs @@ -0,0 +1,7 @@ +namespace MiniSpace.Services.Communication.Application +{ + public class ContractAttribute : Attribute + { + + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Dto/ChatDto.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Dto/ChatDto.cs new file mode 100644 index 000000000..3b7c3224e --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Dto/ChatDto.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; + +namespace MiniSpace.Services.Communication.Application.Dto +{ + public class ChatDto + { + public Guid Id { get; set; } + public string Name { get; set; } + public List ParticipantIds { get; set; } + public List Messages { get; set; } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Dto/MessageDto.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Dto/MessageDto.cs new file mode 100644 index 000000000..d4970dd25 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Dto/MessageDto.cs @@ -0,0 +1,15 @@ +using System; + +namespace MiniSpace.Services.Communication.Application.Dto +{ + public class MessageDto + { + public Guid Id { get; set; } + public Guid ChatId { get; set; } + public Guid SenderId { get; set; } + public string Content { get; set; } + public DateTime Timestamp { get; set; } + public string MessageType { get; set; } + public string Status { get; set; } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Dto/UserChatDto.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Dto/UserChatDto.cs new file mode 100644 index 000000000..f2bc7eb2f --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Dto/UserChatDto.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; + +namespace MiniSpace.Services.Communication.Application.Dto +{ + public class UserChatDto + { + public Guid UserId { get; set; } + public List Chats { get; set; } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Dto/UserDto.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Dto/UserDto.cs new file mode 100644 index 000000000..80587cbcf --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Dto/UserDto.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + + +namespace MiniSpace.Services.Communication.Application.Dto +{ + [ExcludeFromCodeCoverage] + public class UserDto + { + public Guid Id { get; set; } + public string Email { get; set; } + public string FirstName { get; set; } + public string LastName { get; set; } + public string ProfileImageUrl { get; set; } + public string Description { get; set; } + public DateTime? DateOfBirth { get; set; } + public bool EmailNotifications { get; set; } + public bool IsBanned { get; set; } + public string State { get; set; } + public DateTime CreatedAt { get; set; } + public string ContactEmail { get; set; } + public string BannerUrl { get; set; } + public string PhoneNumber { get; set; } + public IEnumerable Languages { get; set; } + public IEnumerable Interests { get; set; } + + // public IEnumerable Education { get; set; } + // public IEnumerable Work { get; set; } + public bool IsTwoFactorEnabled { get; set; } + public string TwoFactorSecret { get; set; } + public IEnumerable InterestedInEvents { get; set; } + public IEnumerable SignedUpEvents { get; set; } + public string Country { get; set; } + public string City { get; set; } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Events/ChatCreated.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Events/ChatCreated.cs new file mode 100644 index 000000000..736768c16 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Events/ChatCreated.cs @@ -0,0 +1,18 @@ +using Convey.CQRS.Events; +using System; +using System.Collections.Generic; + +namespace MiniSpace.Services.Communication.Application.Events +{ + public class ChatCreated : IEvent + { + public Guid ChatId { get; set; } + public List ParticipantIds { get; set; } + + public ChatCreated(Guid chatId, List participantIds) + { + ChatId = chatId; + ParticipantIds = participantIds; + } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Events/ChatDeleted.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Events/ChatDeleted.cs new file mode 100644 index 000000000..6c06f258a --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Events/ChatDeleted.cs @@ -0,0 +1,15 @@ +using Convey.CQRS.Events; +using System; + +namespace MiniSpace.Services.Communication.Application.Events +{ + public class ChatDeleted : IEvent + { + public Guid ChatId { get; } + + public ChatDeleted(Guid chatId) + { + ChatId = chatId; + } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Events/MessageSent.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Events/MessageSent.cs new file mode 100644 index 000000000..8d0d17727 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Events/MessageSent.cs @@ -0,0 +1,21 @@ +using Convey.CQRS.Events; +using System; + +namespace MiniSpace.Services.Communication.Application.Events +{ + public class MessageSent : IEvent + { + public Guid ChatId { get; } + public Guid MessageId { get; } + public Guid SenderId { get; } + public string Content { get; } + + public MessageSent(Guid chatId, Guid messageId, Guid senderId, string content) + { + ChatId = chatId; + MessageId = messageId; + SenderId = senderId; + Content = content; + } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Events/MessageStatusUpdated.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Events/MessageStatusUpdated.cs new file mode 100644 index 000000000..359ed6415 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Events/MessageStatusUpdated.cs @@ -0,0 +1,19 @@ +using Convey.CQRS.Events; +using System; + +namespace MiniSpace.Services.Communication.Application.Events +{ + public class MessageStatusUpdated : IEvent + { + public Guid ChatId { get; set; } + public Guid MessageId { get; set; } + public string Status { get; set; } + + public MessageStatusUpdated(Guid chatId, Guid messageId, string status) + { + ChatId = chatId; + MessageId = messageId; + Status = status; + } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Events/Rejected/ChatCreationRejected.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Events/Rejected/ChatCreationRejected.cs new file mode 100644 index 000000000..063316929 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Events/Rejected/ChatCreationRejected.cs @@ -0,0 +1,19 @@ +using Convey.CQRS.Events; +using System; + +namespace MiniSpace.Services.Communication.Application.Events.Rejected +{ + public class ChatCreationRejected : IRejectedEvent + { + public Guid ChatId { get; } + public string Reason { get; } + public string Code { get; } + + public ChatCreationRejected(Guid chatId, string reason, string code) + { + ChatId = chatId; + Reason = reason; + Code = code; + } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Events/Rejected/ChatDeletionRejected.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Events/Rejected/ChatDeletionRejected.cs new file mode 100644 index 000000000..7ea9a517a --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Events/Rejected/ChatDeletionRejected.cs @@ -0,0 +1,19 @@ +using Convey.CQRS.Events; +using System; + +namespace MiniSpace.Services.Communication.Application.Events.Rejected +{ + public class ChatDeletionRejected : IRejectedEvent + { + public Guid ChatId { get; } + public string Reason { get; } + public string Code { get; } + + public ChatDeletionRejected(Guid chatId, string reason, string code) + { + ChatId = chatId; + Reason = reason; + Code = code; + } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Events/Rejected/ChatProcessRejected.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Events/Rejected/ChatProcessRejected.cs new file mode 100644 index 000000000..51363312c --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Events/Rejected/ChatProcessRejected.cs @@ -0,0 +1,19 @@ +using Convey.CQRS.Events; +using System; + +namespace MiniSpace.Services.Communication.Application.Events.Rejected +{ + public class ChatProcessRejected : IRejectedEvent + { + public Guid ChatId { get; } + public string Reason { get; } + public string Code { get; } + + public ChatProcessRejected(Guid chatId, string reason, string code) + { + ChatId = chatId; + Reason = reason; + Code = code; + } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Events/Rejected/MessageProcessRejected.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Events/Rejected/MessageProcessRejected.cs new file mode 100644 index 000000000..334a1f1f5 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Events/Rejected/MessageProcessRejected.cs @@ -0,0 +1,19 @@ +using Convey.CQRS.Events; +using System; + +namespace MiniSpace.Services.Communication.Application.Events.Rejected +{ + public class MessageProcessRejected : IRejectedEvent + { + public Guid MessageId { get; } + public string Reason { get; } + public string Code { get; } + + public MessageProcessRejected(Guid messageId, string reason, string code) + { + MessageId = messageId; + Reason = reason; + Code = code; + } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Events/Rejected/MessageSendRejected.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Events/Rejected/MessageSendRejected.cs new file mode 100644 index 000000000..61629d6a5 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Events/Rejected/MessageSendRejected.cs @@ -0,0 +1,22 @@ +using Convey.CQRS.Events; +using System; + +namespace MiniSpace.Services.Communication.Application.Events.Rejected +{ + public class MessageSendRejected : IRejectedEvent + { + public Guid ChatId { get; } + public Guid MessageId { get; } + public string Reason { get; } + public string Code { get; } + + public MessageSendRejected(Guid chatId, Guid messageId, string reason, string code) + { + ChatId = chatId; + MessageId = messageId; + Reason = reason; + Code = code; + } + } + +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Events/Rejected/UserAdditionToChatRejected.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Events/Rejected/UserAdditionToChatRejected.cs new file mode 100644 index 000000000..5589a4c38 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Events/Rejected/UserAdditionToChatRejected.cs @@ -0,0 +1,21 @@ +using Convey.CQRS.Events; +using System; + +namespace MiniSpace.Services.Communication.Application.Events.Rejected +{ + public class UserAdditionToChatRejected : IRejectedEvent + { + public Guid ChatId { get; } + public Guid UserId { get; } + public string Reason { get; } + public string Code { get; } + + public UserAdditionToChatRejected(Guid chatId, Guid userId, string reason, string code) + { + ChatId = chatId; + UserId = userId; + Reason = reason; + Code = code; + } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Events/UserAddedToChat.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Events/UserAddedToChat.cs new file mode 100644 index 000000000..84c8c1de1 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Events/UserAddedToChat.cs @@ -0,0 +1,17 @@ +using Convey.CQRS.Events; +using System; + +namespace MiniSpace.Services.Communication.Application.Events +{ + public class UserAddedToChat : IEvent + { + public Guid ChatId { get; } + public Guid UserId { get; } + + public UserAddedToChat(Guid chatId, Guid userId) + { + ChatId = chatId; + UserId = userId; + } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Exceptions/AppException.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Exceptions/AppException.cs new file mode 100644 index 000000000..78c42cdd9 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Exceptions/AppException.cs @@ -0,0 +1,11 @@ +namespace MiniSpace.Services.Communication.Application.Exceptions +{ + public class AppException : Exception + { + public virtual string Code { get; } + + protected AppException(string message) : base(message) + { + } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Exceptions/ChatNotFoundException.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Exceptions/ChatNotFoundException.cs new file mode 100644 index 000000000..af906f1f9 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Exceptions/ChatNotFoundException.cs @@ -0,0 +1,17 @@ +using System; + +namespace MiniSpace.Services.Communication.Application.Exceptions +{ + public class ChatNotFoundException : AppException + { + public override string Code { get; } = "chat_not_found"; + + public Guid ChatId { get; } + + public ChatNotFoundException(Guid chatId) + : base($"Chat with ID '{chatId}' was not found.") + { + ChatId = chatId; + } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Exceptions/InvalidChatOperationException.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Exceptions/InvalidChatOperationException.cs new file mode 100644 index 000000000..ff06611ad --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Exceptions/InvalidChatOperationException.cs @@ -0,0 +1,14 @@ +using System; + +namespace MiniSpace.Services.Communication.Application.Exceptions +{ + public class InvalidChatOperationException : AppException + { + public override string Code { get; } = "invalid_chat_operation"; + + public InvalidChatOperationException(string message) + : base(message) + { + } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Exceptions/MessageNotFoundException.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Exceptions/MessageNotFoundException.cs new file mode 100644 index 000000000..6b384f680 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Exceptions/MessageNotFoundException.cs @@ -0,0 +1,17 @@ +using System; + +namespace MiniSpace.Services.Communication.Application.Exceptions +{ + public class MessageNotFoundException : AppException + { + public override string Code { get; } = "message_not_found"; + + public Guid MessageId { get; } + + public MessageNotFoundException(Guid messageId) + : base($"Message with ID '{messageId}' was not found.") + { + MessageId = messageId; + } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Extensions.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Extensions.cs new file mode 100644 index 000000000..d03c02a20 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Extensions.cs @@ -0,0 +1,16 @@ +using Convey; +using Convey.CQRS.Commands; +using Convey.CQRS.Events; + +namespace MiniSpace.Services.Communication.Application +{ + public static class Extensions + { + public static IConveyBuilder AddApplication(this IConveyBuilder builder) + => builder + .AddCommandHandlers() + .AddEventHandlers() + .AddInMemoryCommandDispatcher() + .AddInMemoryEventDispatcher(); + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Hubs/ChatHub.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Hubs/ChatHub.cs new file mode 100644 index 000000000..d570aec26 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Hubs/ChatHub.cs @@ -0,0 +1,108 @@ +using Microsoft.AspNetCore.SignalR; +using MiniSpace.Services.Communication.Application.Dto; +using System.Text.Json; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; + +namespace MiniSpace.Services.Communication.Application.Hubs +{ + public class ChatHub : Hub + { + private readonly ILogger _logger; + + public ChatHub(ILogger logger) + { + _logger = logger; + } + + public override async Task OnConnectedAsync() + { + var userId = Context.GetHttpContext().Request.Query["userId"]; + if (!string.IsNullOrEmpty(userId)) + { + await Groups.AddToGroupAsync(Context.ConnectionId, userId); + _logger.LogInformation($"User {userId} connected and added to group with Connection ID: {Context.ConnectionId}"); + } + else + { + _logger.LogWarning("User ID is missing in the query string."); + } + await base.OnConnectedAsync(); + } + + public override async Task OnDisconnectedAsync(Exception exception) + { + var userId = Context.GetHttpContext().Request.Query["userId"]; + if (!string.IsNullOrEmpty(userId)) + { + await Groups.RemoveFromGroupAsync(Context.ConnectionId, userId); + _logger.LogInformation($"User {userId} disconnected and removed from group with Connection ID: {Context.ConnectionId}"); + } + await base.OnDisconnectedAsync(exception); + } + + public async Task SendMessage(string userId, MessageDto message) + { + var jsonMessage = JsonSerializer.Serialize(message); + _logger.LogInformation($"Sending message to user {userId}: {jsonMessage}"); + await Clients.User(userId).SendAsync("ReceiveMessage", jsonMessage); + } + + public async Task SendMessageToGroup(string groupName, MessageDto message) + { + var jsonMessage = JsonSerializer.Serialize(message); + _logger.LogInformation($"Sending message to group {groupName}: {jsonMessage}"); + await Clients.Group(groupName).SendAsync("ReceiveGroupMessage", jsonMessage); + } + + public async Task AddToGroup(string groupName) + { + _logger.LogInformation($"Adding connection {Context.ConnectionId} to group {groupName}"); + await Groups.AddToGroupAsync(Context.ConnectionId, groupName); + } + + public async Task RemoveFromGroup(string groupName) + { + _logger.LogInformation($"Removing connection {Context.ConnectionId} from group {groupName}"); + await Groups.RemoveFromGroupAsync(Context.ConnectionId, groupName); + } + + public static async Task SendMessageToUser(IHubContext hubContext, string userId, MessageDto message, ILogger logger) + { + var jsonMessage = JsonSerializer.Serialize(message); + logger.LogInformation($"Sending message to user {userId}: {jsonMessage}"); + await hubContext.Clients.All.SendAsync("ReceiveMessage", jsonMessage); + } + + public static async Task BroadcastMessage(IHubContext hubContext, MessageDto message, ILogger logger) + { + var jsonMessage = JsonSerializer.Serialize(message); + logger.LogInformation($"Broadcasting message to all users: {jsonMessage}"); + await hubContext.Clients.All.SendAsync("ReceiveMessage", jsonMessage); + } + + + public static async Task SendMessageStatusUpdate(IHubContext hubContext, string chatId, Guid messageId, string status, ILogger logger) + { + var statusUpdate = new + { + ChatId = chatId, + MessageId = messageId, + Status = status + }; + + logger.LogInformation($"Sending message status update to chat {chatId} for message {messageId} with status {status}"); + + var jsonStatusUpdate = JsonSerializer.Serialize(statusUpdate); + + await hubContext.Clients.All.SendAsync("ReceiveMessageStatusUpdate", jsonStatusUpdate); + } + + public async Task SendTypingNotification(string chatId, string userId, bool isTyping) + { + _logger.LogInformation($"User {userId} is typing in chat {chatId}: {isTyping}"); + await Clients.All.SendAsync("ReceiveTypingNotification", userId, isTyping); + } + + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/IAppContext.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/IAppContext.cs new file mode 100644 index 000000000..c4b6a6756 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/IAppContext.cs @@ -0,0 +1,8 @@ +namespace MiniSpace.Services.Communication.Application +{ + public interface IAppContext + { + string RequestId { get; } + IIdentityContext Identity { get; } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/IIdentityContext.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/IIdentityContext.cs new file mode 100644 index 000000000..50c867b07 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/IIdentityContext.cs @@ -0,0 +1,14 @@ +namespace MiniSpace.Services.Communication.Application +{ + public interface IIdentityContext + { + Guid Id { get; } + string Role { get; } + string Name { get; } + string Email { get; } + bool IsAuthenticated { get; } + bool IsAdmin { get; } + bool IsBanned { get; } + IDictionary Claims { get; } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/MiniSpace.Services.Communication.Application.csproj b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/MiniSpace.Services.Communication.Application.csproj new file mode 100644 index 000000000..021af6a32 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/MiniSpace.Services.Communication.Application.csproj @@ -0,0 +1,21 @@ + + + + net8.0 + enable + disable + + + + + + + + + + + + + + + diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/MiniSpace.Services.Communication.Application.sln b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/MiniSpace.Services.Communication.Application.sln new file mode 100644 index 000000000..88ccceadd --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/MiniSpace.Services.Communication.Application.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.002.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MiniSpace.Services.Communication.Application", "MiniSpace.Services.Communication.Application.csproj", "{85F8B10B-A401-4C70-9202-F2A8C1C7AF61}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {85F8B10B-A401-4C70-9202-F2A8C1C7AF61}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {85F8B10B-A401-4C70-9202-F2A8C1C7AF61}.Debug|Any CPU.Build.0 = Debug|Any CPU + {85F8B10B-A401-4C70-9202-F2A8C1C7AF61}.Release|Any CPU.ActiveCfg = Release|Any CPU + {85F8B10B-A401-4C70-9202-F2A8C1C7AF61}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {AE2AFDD2-927B-4122-835F-837E29C3330F} + EndGlobalSection +EndGlobal diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Queries/GetChatById.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Queries/GetChatById.cs new file mode 100644 index 000000000..db0a551a5 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Queries/GetChatById.cs @@ -0,0 +1,16 @@ +using Convey.CQRS.Queries; +using MiniSpace.Services.Communication.Application.Dto; +using System; + +namespace MiniSpace.Services.Communication.Application.Queries +{ + public class GetChatById : IQuery + { + public Guid ChatId { get; } + + public GetChatById(Guid chatId) + { + ChatId = chatId; + } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Queries/GetMessagesForChat.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Queries/GetMessagesForChat.cs new file mode 100644 index 000000000..fb7d10508 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Queries/GetMessagesForChat.cs @@ -0,0 +1,17 @@ +using Convey.CQRS.Queries; +using MiniSpace.Services.Communication.Application.Dto; +using System; +using System.Collections.Generic; + +namespace MiniSpace.Services.Communication.Application.Queries +{ + public class GetMessagesForChat : IQuery> + { + public Guid ChatId { get; } + + public GetMessagesForChat(Guid chatId) + { + ChatId = chatId; + } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Queries/GetUserChats.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Queries/GetUserChats.cs new file mode 100644 index 000000000..1728621e0 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Queries/GetUserChats.cs @@ -0,0 +1,21 @@ +using Convey.CQRS.Queries; +using MiniSpace.Services.Communication.Application.Dto; +using System; +using MiniSpace.Services.Communication.Core.Wrappers; + +namespace MiniSpace.Services.Communication.Application.Queries +{ + public class GetUserChats : IQuery> + { + public Guid UserId { get; } + public int Page { get; } + public int PageSize { get; } + + public GetUserChats(Guid userId, int page, int pageSize) + { + UserId = userId; + Page = page > 0 ? page : 1; + PageSize = pageSize > 0 ? pageSize : 10; + } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Services/Clients/IStudentsServiceClient.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Services/Clients/IStudentsServiceClient.cs new file mode 100644 index 000000000..8dd50b92f --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Services/Clients/IStudentsServiceClient.cs @@ -0,0 +1,11 @@ +using System; +using System.Threading.Tasks; +using MiniSpace.Services.Communication.Application.Dto; + +namespace MiniSpace.Services.Communication.Application.Services.Clients +{ + public interface IStudentsServiceClient + { + Task GetAsync(Guid id); + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Services/CommunicationService.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Services/CommunicationService.cs new file mode 100644 index 000000000..96f67ad1d --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Services/CommunicationService.cs @@ -0,0 +1,124 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using MiniSpace.Services.Communication.Application.Commands; +using MiniSpace.Services.Communication.Application.Events; +using MiniSpace.Services.Communication.Core.Entities; +using MiniSpace.Services.Communication.Core.Repositories; + +namespace MiniSpace.Services.Communication.Application.Services +{ + public class CommunicationService : ICommunicationService + { + private readonly IUserChatsRepository _userChatsRepository; + private readonly IMessageBroker _messageBroker; + private readonly ILogger _logger; + + public CommunicationService(IUserChatsRepository userChatsRepository, IMessageBroker messageBroker, ILogger logger) + { + _userChatsRepository = userChatsRepository; + _messageBroker = messageBroker; + _logger = logger; + } + + public async Task CreateChatAsync(Guid chatId, List participantIds, string chatName = null) + { + _logger.LogInformation($"Creating chat with ID: {chatId}"); + + // Check if the chat already exists + Chat existingChat = null; + foreach (var participantId in participantIds) + { + var userChats = await _userChatsRepository.GetByUserIdAsync(participantId); + if (userChats != null) + { + existingChat = userChats.Chats.Find(chat => + chat.ParticipantIds.Count == participantIds.Count && + chat.ParticipantIds.All(pid => participantIds.Contains(pid)) + ); + + if (existingChat != null) + { + _logger.LogWarning($"Chat with ID: {existingChat.Id} already exists. Returning existing chat ID."); + return existingChat.Id; + } + } + } + + // Create a new chat if none exists + var newChat = new Chat(chatId, participantIds, new List()); + foreach (var participantId in participantIds) + { + var userChats = await _userChatsRepository.GetByUserIdAsync(participantId) ?? new UserChats(participantId); + userChats.AddChat(newChat); + await _userChatsRepository.AddOrUpdateAsync(userChats); + } + + await _messageBroker.PublishAsync(new ChatCreated(newChat.Id, participantIds)); + + _logger.LogInformation($"New chat created with ID: {newChat.Id}"); + return newChat.Id; + } + + + public async Task UpdateMessageStatusAsync(Guid chatId, Guid messageId, string status) + { + // Retrieve the chat using the GetByChatIdAsync method + var chat = await _userChatsRepository.GetByChatIdAsync(chatId); + if (chat == null) + { + _logger.LogWarning($"Chat with ID {chatId} not found."); + return; + } + + var message = chat.Messages.FirstOrDefault(m => m.Id == messageId); + if (message == null) + { + _logger.LogWarning($"Message with ID {messageId} not found in chat with ID {chatId}."); + return; + } + + switch (status) + { + case "Read": + message.MarkAsRead(); + break; + case "Unread": + message.MarkAsUnread(); + break; + case "Deleted": + message.MarkAsDeleted(); + break; + default: + _logger.LogWarning($"Unsupported status '{status}' for message ID {messageId} in chat ID {chatId}."); + return; + } + + foreach (var participantId in chat.ParticipantIds) + { + var userChats = await _userChatsRepository.GetByUserIdAsync(participantId); + if (userChats != null) + { + var existingChat = userChats.GetChatById(chatId); + if (existingChat != null) + { + var messageToUpdate = existingChat.Messages.FirstOrDefault(m => m.Id == messageId); + if (messageToUpdate != null) + { + _logger.LogInformation($"Updating message status for chat ID {chatId} and message ID {messageId} to {status}"); + messageToUpdate.MarkAsRead(); + } + _logger.LogInformation($"Updating chat for participant {participantId} with chat ID {chatId}"); + await _userChatsRepository.UpdateAsync(userChats); + } + } + } + + _logger.LogInformation($"Message status updated: ChatId={chatId}, MessageId={messageId}, Status={status}"); + + await _messageBroker.PublishAsync(new MessageStatusUpdated(chatId, messageId, status)); + } + + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Services/ICommunicationService.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Services/ICommunicationService.cs new file mode 100644 index 000000000..da0dd02cd --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Services/ICommunicationService.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using MiniSpace.Services.Communication.Application.Commands; +using MiniSpace.Services.Communication.Application.Dto; + +namespace MiniSpace.Services.Communication.Application.Services +{ + public interface ICommunicationService + { + Task CreateChatAsync(Guid chatId, List participantIds, string chatName = null); + Task UpdateMessageStatusAsync(Guid chatId, Guid messageId, string status); + // Task> GetUserChatsAsync(Guid userId); + // Task GetChatByIdAsync(Guid chatId); + // Task SendMessageAsync(SendMessage command); + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Services/IDateTimeProvider.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Services/IDateTimeProvider.cs new file mode 100644 index 000000000..527fa2c7e --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Services/IDateTimeProvider.cs @@ -0,0 +1,7 @@ +namespace MiniSpace.Services.Communication.Application.Services +{ + public interface IDateTimeProvider + { + DateTime Now { get; } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Services/IEventMapper.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Services/IEventMapper.cs new file mode 100644 index 000000000..0cde987ee --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Services/IEventMapper.cs @@ -0,0 +1,11 @@ +using Convey.CQRS.Events; +using MiniSpace.Services.Communication.Core.Events; + +namespace MiniSpace.Services.Communication.Application.Services +{ + public interface IEventMapper + { + IEvent Map(IDomainEvent @event); + IEnumerable MapAll(IEnumerable events); + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Services/IMessageBroker.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Services/IMessageBroker.cs new file mode 100644 index 000000000..405bad2e8 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Services/IMessageBroker.cs @@ -0,0 +1,10 @@ +using Convey.CQRS.Events; + +namespace MiniSpace.Services.Communication.Application.Services +{ + public interface IMessageBroker + { + Task PublishAsync(params IEvent[] events); + Task PublishAsync(IEnumerable events); + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Services/ISignalRConnectionManager.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Services/ISignalRConnectionManager.cs new file mode 100644 index 000000000..1df25a459 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Services/ISignalRConnectionManager.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace MiniSpace.Services.Communication.Application.Services +{ + public interface ISignalRConnectionManager + { + Task SendMessageAsync(string user, string message); + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/.gitignore b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/.gitignore new file mode 100644 index 000000000..1f7e99963 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/.gitignore @@ -0,0 +1,334 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ +# **/Properties/launchSettings.json + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +bin/ +obj/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush +.cr/ + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +logs/ diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/Entities/AggregateId.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/Entities/AggregateId.cs new file mode 100644 index 000000000..63fe3517a --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/Entities/AggregateId.cs @@ -0,0 +1,50 @@ +using MiniSpace.Services.Communication.Core.Exceptions; + +namespace MiniSpace.Services.Communication.Core.Entities +{ + public class AggregateId : IEquatable + { + public Guid Value { get; } + + public AggregateId() + { + Value = Guid.NewGuid(); + } + + public AggregateId(Guid value) + { + if (value == Guid.Empty) + { + throw new InvalidAggregateIdException(); + } + + Value = value; + } + + public bool Equals(AggregateId other) + { + if (ReferenceEquals(null, other)) return false; + return ReferenceEquals(this, other) || Value.Equals(other.Value); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + return obj.GetType() == GetType() && Equals((AggregateId) obj); + } + + public override int GetHashCode() + { + return Value.GetHashCode(); + } + + public static implicit operator Guid(AggregateId id) + => id.Value; + + public static implicit operator AggregateId(Guid id) + => new AggregateId(id); + + public override string ToString() => Value.ToString(); + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/Entities/AggregateRoot.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/Entities/AggregateRoot.cs new file mode 100644 index 000000000..7d5382101 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/Entities/AggregateRoot.cs @@ -0,0 +1,19 @@ +using MiniSpace.Services.Communication.Core.Events; + +namespace MiniSpace.Services.Communication.Core.Entities +{ + public abstract class AggregateRoot + { + private readonly List _events = new List(); + public IEnumerable Events => _events; + public AggregateId Id { get; protected set; } + public int Version { get; protected set; } + + protected void AddEvent(IDomainEvent @event) + { + _events.Add(@event); + } + + public void ClearEvents() => _events.Clear(); + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/Entities/Chat.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/Entities/Chat.cs new file mode 100644 index 000000000..2624138a9 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/Entities/Chat.cs @@ -0,0 +1,31 @@ +namespace MiniSpace.Services.Communication.Core.Entities +{ + public class Chat : AggregateRoot + { + public Guid Id { get; private set; } + public List ParticipantIds { get; private set; } + public List Messages { get; private set; } + + // Constructor for creating a new chat with a new Id + public Chat(List participantIds) + { + Id = Guid.NewGuid(); + ParticipantIds = participantIds; + Messages = new List(); + } + + // Constructor for loading an existing chat from the database + public Chat(Guid id, List participantIds, List messages) + { + Id = id; + ParticipantIds = participantIds; + Messages = messages; + } + + public void AddMessage(Message message) + { + Messages.Add(message); + AddEvent(new Events.MessageAddedEvent(Id, message.Id)); + } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/Entities/Message.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/Entities/Message.cs new file mode 100644 index 000000000..d9023b3b4 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/Entities/Message.cs @@ -0,0 +1,60 @@ +using System; + +namespace MiniSpace.Services.Communication.Core.Entities +{ + public class Message + { + public Guid Id { get; private set; } + public Guid SenderId { get; private set; } + public Guid ReceiverId { get; private set; } + public Guid ChatId { get; private set; } + public string Content { get; private set; } + public DateTime Timestamp { get; private set; } + public MessageType Type { get; private set; } + public MessageStatus Status { get; private set; } + + // Constructor for creating a new message + public Message(Guid chatId, Guid senderId, Guid receiverId, string content, MessageType type) + { + Id = Guid.NewGuid(); // Generate a new ID for a new message + ChatId = chatId; + SenderId = senderId; + ReceiverId = receiverId; + Content = content; + Timestamp = DateTime.UtcNow; + Type = type; + Status = MessageStatus.Sent; + } + + // Constructor for loading an existing message from the database + public Message(Guid id, Guid chatId, Guid senderId, Guid receiverId, string content, DateTime timestamp, MessageType type, MessageStatus status) + { + Id = id; // Use the existing ID from the database + ChatId = chatId; + SenderId = senderId; + ReceiverId = receiverId; + Content = content; + Timestamp = timestamp; + Type = type; + Status = status; + } + + public void MarkAsRead() + { + Status = MessageStatus.Read; + } + + public void MarkAsUnread() + { + if (Status == MessageStatus.Read) + { + Status = MessageStatus.Unread; + } + } + + public void MarkAsDeleted() + { + Status = MessageStatus.Deleted; + } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/Entities/MessageStatus.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/Entities/MessageStatus.cs new file mode 100644 index 000000000..54a0ff61f --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/Entities/MessageStatus.cs @@ -0,0 +1,11 @@ +namespace MiniSpace.Services.Communication.Core.Entities +{ + public enum MessageStatus + { + Sent, + Delivered, + Unread, + Read, + Deleted + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/Entities/MessageType.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/Entities/MessageType.cs new file mode 100644 index 000000000..67981b777 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/Entities/MessageType.cs @@ -0,0 +1,10 @@ +namespace MiniSpace.Services.Communication.Core.Entities +{ + public enum MessageType + { + Text, + Image, + Video, + File + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/Entities/OrganizationChats.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/Entities/OrganizationChats.cs new file mode 100644 index 000000000..b8d75b4f3 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/Entities/OrganizationChats.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace MiniSpace.Services.Communication.Core.Entities +{ + public class OrganizationChats + { + public Guid OrganizationId { get; private set; } + public List Chats { get; private set; } + + public OrganizationChats(Guid organizationId) + { + OrganizationId = organizationId; + Chats = new List(); + } + + public void AddChat(Chat chat) + { + if (chat == null) + throw new ArgumentNullException(nameof(chat)); + + Chats.Add(chat); + } + + public void RemoveChat(Guid chatId) + { + var chat = Chats.FirstOrDefault(c => c.Id == chatId); + if (chat != null) + { + Chats.Remove(chat); + } + } + + public Chat GetChatById(Guid chatId) + { + return Chats.FirstOrDefault(c => c.Id == chatId); + } + + public bool ChatExists(Guid chatId) + { + return Chats.Any(c => c.Id == chatId); + } + + public void UpdateChat(Chat updatedChat) + { + var existingChat = GetChatById(updatedChat.Id); + if (existingChat != null) + { + Chats.Remove(existingChat); + Chats.Add(updatedChat); + } + } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/Entities/ReactionType.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/Entities/ReactionType.cs new file mode 100644 index 000000000..5625742ab --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/Entities/ReactionType.cs @@ -0,0 +1,11 @@ +namespace MiniSpace.Services.Communication.Core.Entities +{ + public enum ReactionType + { + LoveIt, + LikeIt, + Wow, + ItWasOkay, + HateIt + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/Entities/UserChats.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/Entities/UserChats.cs new file mode 100644 index 000000000..c77922ee1 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/Entities/UserChats.cs @@ -0,0 +1,24 @@ +namespace MiniSpace.Services.Communication.Core.Entities +{ + public class UserChats + { + public Guid UserId { get; private set; } + public List Chats { get; private set; } + + public UserChats(Guid userId) + { + UserId = userId; + Chats = new List(); + } + + public void AddChat(Chat chat) + { + Chats.Add(chat); + } + + public Chat GetChatById(Guid chatId) + { + return Chats.FirstOrDefault(chat => chat.Id == chatId); + } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/Entities/UserMessages.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/Entities/UserMessages.cs new file mode 100644 index 000000000..06d5b1cc6 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/Entities/UserMessages.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace MiniSpace.Services.Communication.Core.Entities +{ + public class UserMessages + { + public Guid UserId { get; private set; } + private List _messages; + + public IEnumerable Messages => _messages.AsReadOnly(); + + public UserMessages(Guid userId) + { + UserId = userId; + _messages = new List(); + } + + public void AddMessage(Message message) + { + if (message != null) + { + _messages.Add(message); + } + } + + public void RemoveMessage(Guid messageId) + { + _messages.RemoveAll(m => m.Id == messageId); + } + + public void MarkMessageAsRead(Guid messageId) + { + var message = _messages.FirstOrDefault(m => m.Id == messageId); + if (message != null) + { + message.MarkAsRead(); + } + } + + public void MarkMessageAsUnread(Guid messageId) + { + var message = _messages.FirstOrDefault(m => m.Id == messageId); + if (message != null) + { + message.MarkAsUnread(); + } + } + + public IEnumerable GetMessagesForChat(Guid chatId) + { + return _messages.Where(m => m.ChatId == chatId); + } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/Events/IDomainEvent.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/Events/IDomainEvent.cs new file mode 100644 index 000000000..c9c698407 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/Events/IDomainEvent.cs @@ -0,0 +1,7 @@ +namespace MiniSpace.Services.Communication.Core.Events +{ + public interface IDomainEvent + { + + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/Events/MessageAddedEvent.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/Events/MessageAddedEvent.cs new file mode 100644 index 000000000..014121ee9 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/Events/MessageAddedEvent.cs @@ -0,0 +1,14 @@ +namespace MiniSpace.Services.Communication.Core.Events +{ + public class MessageAddedEvent : IDomainEvent + { + public Guid ChatId { get; } + public Guid MessageId { get; } + + public MessageAddedEvent(Guid chatId, Guid messageId) + { + ChatId = chatId; + MessageId = messageId; + } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/Exceptions/DomainException.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/Exceptions/DomainException.cs new file mode 100644 index 000000000..db88f3978 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/Exceptions/DomainException.cs @@ -0,0 +1,11 @@ +namespace MiniSpace.Services.Communication.Core.Exceptions +{ + public abstract class DomainException : Exception + { + public virtual string Code { get; } + + protected DomainException(string message) : base(message) + { + } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/Exceptions/InvalidAggregateIdException.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/Exceptions/InvalidAggregateIdException.cs new file mode 100644 index 000000000..0c439e479 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/Exceptions/InvalidAggregateIdException.cs @@ -0,0 +1,11 @@ +namespace MiniSpace.Services.Communication.Core.Exceptions +{ + public class InvalidAggregateIdException : DomainException + { + public override string Code { get; } = "invalid_aggregate_id"; + + public InvalidAggregateIdException() : base($"Invalid aggregate id.") + { + } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/MiniSpace.Services.Communication.Core.csproj b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/MiniSpace.Services.Communication.Core.csproj new file mode 100644 index 000000000..cf309aa85 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/MiniSpace.Services.Communication.Core.csproj @@ -0,0 +1,9 @@ + + + + net8.0 + enable + disable + + + diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/MiniSpace.Services.Communication.Core.sln b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/MiniSpace.Services.Communication.Core.sln new file mode 100644 index 000000000..92b21c3c4 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/MiniSpace.Services.Communication.Core.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.002.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MiniSpace.Services.Communication.Core", "MiniSpace.Services.Communication.Core.csproj", "{42E0A571-69FB-45ED-BACA-E0F72B9C0DE1}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {42E0A571-69FB-45ED-BACA-E0F72B9C0DE1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {42E0A571-69FB-45ED-BACA-E0F72B9C0DE1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {42E0A571-69FB-45ED-BACA-E0F72B9C0DE1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {42E0A571-69FB-45ED-BACA-E0F72B9C0DE1}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {0A93A2FC-8B65-4C19-A90B-588F58354A02} + EndGlobalSection +EndGlobal diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/Repositories/IOrganizationChatsRepository.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/Repositories/IOrganizationChatsRepository.cs new file mode 100644 index 000000000..5fcbf9835 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/Repositories/IOrganizationChatsRepository.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using MiniSpace.Services.Communication.Core.Entities; + +namespace MiniSpace.Services.Communication.Core.Repositories +{ + public interface IOrganizationChatsRepository + { + Task GetByOrganizationIdAsync(Guid organizationId); + Task AddAsync(OrganizationChats organizationChats); + Task UpdateAsync(OrganizationChats organizationChats); + Task AddOrUpdateAsync(OrganizationChats organizationChats); + Task DeleteAsync(Guid organizationId); + Task ChatExistsAsync(Guid organizationId, Guid chatId); + Task AddChatAsync(Guid organizationId, Chat chat); + Task DeleteChatAsync(Guid organizationId, Guid chatId); + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/Repositories/IUserChatsRepository.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/Repositories/IUserChatsRepository.cs new file mode 100644 index 000000000..204c8b668 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/Repositories/IUserChatsRepository.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using MiniSpace.Services.Communication.Core.Entities; + +namespace MiniSpace.Services.Communication.Core.Repositories +{ + public interface IUserChatsRepository + { + Task GetByUserIdAsync(Guid userId); + Task AddAsync(UserChats userChats); + Task UpdateAsync(UserChats userChats); + Task AddOrUpdateAsync(UserChats userChats); + Task DeleteAsync(Guid userId); + Task ChatExistsAsync(Guid userId, Guid chatId); + Task AddChatAsync(Guid userId, Chat chat); + Task DeleteChatAsync(Guid userId, Guid chatId); + Task GetByChatIdAsync(Guid chatId); + Task> GetParticipantIdsByChatIdAsync(Guid chatId); + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/Wrappers/PagedResponse.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/Wrappers/PagedResponse.cs new file mode 100644 index 000000000..586f3a6ab --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/Wrappers/PagedResponse.cs @@ -0,0 +1,28 @@ +namespace MiniSpace.Services.Communication.Core.Wrappers +{ + public class PagedResponse + { + public IEnumerable Items { get; } + public int TotalPages { get; } + public int TotalItems { get; } + public int PageSize { get; } + public int Page { get; } + public bool First { get; } + public bool Last { get; } + public bool Empty { get; } + public int? NextPage => Page < TotalPages ? Page + 1 : (int?)null; + public int? PreviousPage => Page > 1 ? Page - 1 : (int?)null; + + public PagedResponse(IEnumerable items, int page, int pageSize, int totalItems) + { + Items = items; + PageSize = pageSize; + TotalItems = totalItems; + TotalPages = pageSize > 0 ? (int)Math.Ceiling((decimal)totalItems / pageSize) : 0; + Page = page; + First = page == 1; + Last = page == TotalPages; + Empty = !items.Any(); + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/Wrappers/Response.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/Wrappers/Response.cs new file mode 100644 index 000000000..92816d60c --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/Wrappers/Response.cs @@ -0,0 +1,10 @@ +namespace MiniSpace.Services.Communication.Core.Wrappers +{ + public class Response + { + public T Content { get; set; } + public bool Succeeded { get; set; } + public string[] Errors { get; set; } + public string Message { get; set; } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/.gitignore b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/.gitignore new file mode 100644 index 000000000..1f7e99963 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/.gitignore @@ -0,0 +1,334 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ +# **/Properties/launchSettings.json + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +bin/ +obj/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush +.cr/ + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +logs/ diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Contexts/AppContext.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Contexts/AppContext.cs new file mode 100644 index 000000000..aa6839fbd --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Contexts/AppContext.cs @@ -0,0 +1,27 @@ +using MiniSpace.Services.Communication.Application; + +namespace MiniSpace.Services.Communication.Infrastructure.Contexts +{ + internal class AppContext : IAppContext + { + public string RequestId { get; } + public IIdentityContext Identity { get; } + + internal AppContext() : this(Guid.NewGuid().ToString("N"), IdentityContext.Empty) + { + } + + internal AppContext(CorrelationContext context) : this(context.CorrelationId, + context.User is null ? IdentityContext.Empty : new IdentityContext(context.User)) + { + } + + internal AppContext(string requestId, IIdentityContext identity) + { + RequestId = requestId; + Identity = identity; + } + + internal static IAppContext Empty => new AppContext(); + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Contexts/AppContextFactory.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Contexts/AppContextFactory.cs new file mode 100644 index 000000000..8f300c3d8 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Contexts/AppContextFactory.cs @@ -0,0 +1,36 @@ +using Convey.MessageBrokers; +using Microsoft.AspNetCore.Http; +using Newtonsoft.Json; +using MiniSpace.Services.Communication.Application; +using MiniSpace.Services.Communication.Infrastructure; + +namespace MiniSpace.Services.Communication.Infrastructure.Contexts +{ + internal sealed class AppContextFactory : IAppContextFactory + { + private readonly ICorrelationContextAccessor _contextAccessor; + private readonly IHttpContextAccessor _httpContextAccessor; + + public AppContextFactory(ICorrelationContextAccessor contextAccessor, IHttpContextAccessor httpContextAccessor) + { + _contextAccessor = contextAccessor; + _httpContextAccessor = httpContextAccessor; + } + + public IAppContext Create() + { + if (_contextAccessor.CorrelationContext is { }) + { + var payload = JsonConvert.SerializeObject(_contextAccessor.CorrelationContext); + + return string.IsNullOrWhiteSpace(payload) + ? AppContext.Empty + : new AppContext(JsonConvert.DeserializeObject(payload)); + } + + var context = _httpContextAccessor.GetCorrelationContext(); + + return context is null ? AppContext.Empty : new AppContext(context); + } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Contexts/CorrelationContext.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Contexts/CorrelationContext.cs new file mode 100644 index 000000000..8c2def9a0 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Contexts/CorrelationContext.cs @@ -0,0 +1,22 @@ +namespace MiniSpace.Services.Communication.Infrastructure.Contexts +{ + internal class CorrelationContext + { + public string CorrelationId { get; set; } + public string SpanContext { get; set; } + public UserContext User { get; set; } + public string ResourceId { get; set; } + public string TraceId { get; set; } + public string ConnectionId { get; set; } + public string Name { get; set; } + public DateTime CreatedAt { get; set; } + + public class UserContext + { + public string Id { get; set; } + public bool IsAuthenticated { get; set; } + public string Role { get; set; } + public IDictionary Claims { get; set; } + } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Contexts/IdentityContext.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Contexts/IdentityContext.cs new file mode 100644 index 000000000..1e06fc482 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Contexts/IdentityContext.cs @@ -0,0 +1,39 @@ +using MiniSpace.Services.Communication.Application; + +namespace MiniSpace.Services.Communication.Infrastructure.Contexts +{ + internal class IdentityContext : IIdentityContext + { + public Guid Id { get; } + public string Role { get; } = string.Empty; + public string Name { get; } = string.Empty; + public string Email { get; } = string.Empty; + public bool IsAuthenticated { get; } + public bool IsAdmin { get; } + public bool IsBanned { get; } + public IDictionary Claims { get; } = new Dictionary(); + + internal IdentityContext() + { + } + + internal IdentityContext(CorrelationContext.UserContext context) + : this(context.Id, context.Role, context.IsAuthenticated, context.Claims) + { + } + + internal IdentityContext(string id, string role, bool isAuthenticated, IDictionary claims) + { + Id = Guid.TryParse(id, out var userId) ? userId : Guid.Empty; + Role = role ?? string.Empty; + IsAuthenticated = isAuthenticated; + IsAdmin = Role.Equals("admin", StringComparison.InvariantCultureIgnoreCase); + IsBanned = Role.Equals("banned", StringComparison.InvariantCultureIgnoreCase); + Claims = claims ?? new Dictionary(); + Name = Claims.TryGetValue("name", out var name) ? name : string.Empty; + Email = Claims.TryGetValue("email", out var email) ? email : string.Empty; + } + + internal static IIdentityContext Empty => new IdentityContext(); + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Decorators/OutboxCommandHandlerDecorator.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Decorators/OutboxCommandHandlerDecorator.cs new file mode 100644 index 000000000..849f12031 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Decorators/OutboxCommandHandlerDecorator.cs @@ -0,0 +1,35 @@ +using Convey.CQRS.Commands; +using Convey.MessageBrokers; +using Convey.MessageBrokers.Outbox; +using Convey.Types; + +namespace MiniSpace.Services.Communication.Infrastructure.Decorators +{ + [Decorator] + internal sealed class OutboxCommandHandlerDecorator : ICommandHandler + where TCommand : class, ICommand + { + private readonly ICommandHandler _handler; + private readonly IMessageOutbox _outbox; + private readonly string _messageId; + private readonly bool _enabled; + + public OutboxCommandHandlerDecorator(ICommandHandler handler, IMessageOutbox outbox, + OutboxOptions outboxOptions, IMessagePropertiesAccessor messagePropertiesAccessor) + { + _handler = handler; + _outbox = outbox; + _enabled = outboxOptions.Enabled; + + var messageProperties = messagePropertiesAccessor.MessageProperties; + _messageId = string.IsNullOrWhiteSpace(messageProperties?.MessageId) + ? Guid.NewGuid().ToString("N") + : messageProperties.MessageId; + } + + public Task HandleAsync(TCommand command, CancellationToken cancellationToken) + => _enabled + ? _outbox.HandleAsync(_messageId, () => _handler.HandleAsync(command)) + : _handler.HandleAsync(command); + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Decorators/OutboxEventHandlerDecorator.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Decorators/OutboxEventHandlerDecorator.cs new file mode 100644 index 000000000..3e1e88706 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Decorators/OutboxEventHandlerDecorator.cs @@ -0,0 +1,35 @@ +using Convey.CQRS.Events; +using Convey.MessageBrokers; +using Convey.MessageBrokers.Outbox; +using Convey.Types; + +namespace MiniSpace.Services.Communication.Infrastructure.Decorators +{ + [Decorator] + internal sealed class OutboxEventHandlerDecorator : IEventHandler + where TEvent : class, IEvent + { + private readonly IEventHandler _handler; + private readonly IMessageOutbox _outbox; + private readonly string _messageId; + private readonly bool _enabled; + + public OutboxEventHandlerDecorator(IEventHandler handler, IMessageOutbox outbox, + OutboxOptions outboxOptions, IMessagePropertiesAccessor messagePropertiesAccessor) + { + _handler = handler; + _outbox = outbox; + _enabled = outboxOptions.Enabled; + + var messageProperties = messagePropertiesAccessor.MessageProperties; + _messageId = string.IsNullOrWhiteSpace(messageProperties?.MessageId) + ? Guid.NewGuid().ToString("N") + : messageProperties.MessageId; + } + + public Task HandleAsync(TEvent @event, CancellationToken cancellationToken) + => _enabled + ? _outbox.HandleAsync(_messageId, () => _handler.HandleAsync(@event)) + : _handler.HandleAsync(@event); + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Exceptions/ExceptionToMessageMapper.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Exceptions/ExceptionToMessageMapper.cs new file mode 100644 index 000000000..dd910203c --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Exceptions/ExceptionToMessageMapper.cs @@ -0,0 +1,40 @@ +using Convey.MessageBrokers.RabbitMQ; +using MiniSpace.Services.Communication.Application.Commands; +using MiniSpace.Services.Communication.Application.Events.Rejected; +using MiniSpace.Services.Communication.Application.Exceptions; +using System; + +namespace MiniSpace.Services.Communication.Infrastructure.Exceptions +{ + internal sealed class ExceptionToMessageMapper : IExceptionToMessageMapper + { + public object Map(Exception exception, object message) + => exception switch + { + // ChatNotFoundException ex => message switch + // { + // DeleteChat command => new ChatDeletionRejected(command.ChatId, "Chat not found", ex.Code), + // CreateChat command => new ChatCreationRejected(command.ChatId, "Chat not found", ex.Code), + // AddUserToChat command => new UserAdditionToChatRejected(command.ChatId, command.UserId, "Chat not found", ex.Code), + // SendMessage command => new MessageSendRejected(command.ChatId, Guid.NewGuid(), "Chat not found", ex.Code), + // _ => new ChatProcessRejected(ex.ChatId, ex.Message, ex.Code), + // }, + // MessageNotFoundException ex => message switch + // { + // DeleteMessage command => new MessageSendRejected(command.ChatId, command.MessageId, "Message not found", ex.Code), + // _ => new MessageProcessRejected(ex.MessageId, ex.Message, ex.Code), + // }, + // InvalidChatOperationException ex => message switch + // { + // AddUserToChat command => new UserAdditionToChatRejected(command.ChatId, command.UserId, ex.Message, ex.Code), + // SendMessage command => new MessageSendRejected(command.ChatId, Guid.NewGuid(), ex.Message, ex.Code), + // _ => new ChatProcessRejected(Guid.Empty, ex.Message, ex.Code), + // }, + // AppException ex => message switch + // { + // _ => new ChatProcessRejected(Guid.Empty, ex.Message, ex.Code) + // }, + // _ => null + }; + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Exceptions/ExceptionToResponseMapper.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Exceptions/ExceptionToResponseMapper.cs new file mode 100644 index 000000000..6338b7896 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Exceptions/ExceptionToResponseMapper.cs @@ -0,0 +1,46 @@ +using System.Collections.Concurrent; +using System.Net; +using Convey; +using Convey.WebApi.Exceptions; +using MiniSpace.Services.Communication.Application.Exceptions; +using MiniSpace.Services.Communication.Core.Exceptions; + +namespace MiniSpace.Services.Communication.Infrastructure.Exceptions +{ + internal sealed class ExceptionToResponseMapper : IExceptionToResponseMapper + { + private static readonly ConcurrentDictionary Codes = new ConcurrentDictionary(); + + public ExceptionResponse Map(Exception exception) + => exception switch + { + DomainException ex => new ExceptionResponse(new {code = GetCode(ex), reason = ex.Message}, + HttpStatusCode.BadRequest), + AppException ex => new ExceptionResponse(new {code = GetCode(ex), reason = ex.Message}, + HttpStatusCode.BadRequest), + _ => new ExceptionResponse(new {code = "error", reason = "There was an error."}, + HttpStatusCode.BadRequest) + }; + + private static string GetCode(Exception exception) + { + var type = exception.GetType(); + if (Codes.TryGetValue(type, out var code)) + { + return code; + } + + var exceptionCode = exception switch + { + DomainException domainException when !string.IsNullOrWhiteSpace(domainException.Code) => domainException + .Code, + AppException appException when !string.IsNullOrWhiteSpace(appException.Code) => appException.Code, + _ => exception.GetType().Name.Underscore().Replace("_exception", string.Empty) + }; + + Codes.TryAdd(type, exceptionCode); + + return exceptionCode; + } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Extensions.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Extensions.cs new file mode 100644 index 000000000..a0779ab56 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Extensions.cs @@ -0,0 +1,165 @@ +using System.Text; +using Convey; +using Convey.CQRS.Commands; +using Convey.CQRS.Events; +using Convey.CQRS.Queries; +using Convey.Discovery.Consul; +using Convey.Docs.Swagger; +using Convey.HTTP; +using Convey.LoadBalancing.Fabio; +using Convey.MessageBrokers; +using Convey.MessageBrokers.CQRS; +using Convey.MessageBrokers.Outbox; +using Convey.MessageBrokers.Outbox.Mongo; +using Convey.MessageBrokers.RabbitMQ; +using Convey.Metrics.AppMetrics; +using Convey.Persistence.MongoDB; +using Convey.Persistence.Redis; +using Convey.Security; +using Convey.Tracing.Jaeger; +using Convey.Tracing.Jaeger.RabbitMQ; +using Convey.WebApi; +using Convey.WebApi.CQRS; +using Convey.WebApi.Security; +using Convey.WebApi.Swagger; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Newtonsoft.Json; +using MiniSpace.Services.Communication.Application; +using MiniSpace.Services.Communication.Application.Commands; +using MiniSpace.Services.Communication.Application.Services; +using MiniSpace.Services.Communication.Core.Repositories; +using MiniSpace.Services.Communication.Infrastructure.Contexts; +using MiniSpace.Services.Communication.Infrastructure.Decorators; +using MiniSpace.Services.Communication.Infrastructure.Exceptions; +using MiniSpace.Services.Communication.Infrastructure.Logging; +using MiniSpace.Services.Communication.Infrastructure.Mongo.Documents; +using MiniSpace.Services.Communication.Infrastructure.Mongo.Repositories; +using MiniSpace.Services.Communication.Infrastructure.Services; +using MiniSpace.Services.Communication.Application.Services.Clients; +using MiniSpace.Services.Communication.Infrastructure.Services.Clients; +using MiniSpace.Services.Communication.Application.Hubs; + +namespace MiniSpace.Services.Communication.Infrastructure +{ + public static class Extensions + { + public static IConveyBuilder AddInfrastructure(this IConveyBuilder builder) + { + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + + builder.Services.AddSingleton(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + + builder.Services.AddTransient(ctx => ctx.GetRequiredService().Create()); + builder.Services.TryDecorate(typeof(ICommandHandler<>), typeof(OutboxCommandHandlerDecorator<>)); + builder.Services.TryDecorate(typeof(IEventHandler<>), typeof(OutboxEventHandlerDecorator<>)); + + + return builder + .AddErrorHandler() + .AddQueryHandlers() + .AddInMemoryQueryDispatcher() + .AddHttpClient() + .AddConsul() + .AddFabio() + .AddRabbitMq(plugins: p => p.AddJaegerRabbitMqPlugin()) + .AddMessageOutbox(o => o.AddMongo()) + .AddExceptionToMessageMapper() + .AddMongo() + .AddRedis() + .AddMetrics() + .AddJaeger() + .AddHandlersLogging() + .AddMongoRepository("organizations_chats") + .AddMongoRepository("user_chats") + .AddSignalRInfrastructure() + .AddWebApiSwaggerDocs() + .AddCertificateAuthentication() + .AddSecurity(); + } + + public static IApplicationBuilder UseInfrastructure(this IApplicationBuilder app) + { + app.UseErrorHandler() + .UseSwaggerDocs() + .UseJaeger() + .UseConvey() + .UsePublicContracts() + .UseMetrics() + .UseCertificateAuthentication() + .UseRabbitMq() + .SubscribeCommand() + .SubscribeCommand() + .SubscribeCommand() + .SubscribeCommand() + .SubscribeCommand() + .SubscribeCommand() + .SubscribeCommand() + ; + return app; + } + + public static IConveyBuilder AddSignalRInfrastructure(this IConveyBuilder builder) + { + builder.Services.AddCors(options => + { + options.AddPolicy("CorsPolicy", + builder => builder + .AllowAnyMethod() + .AllowAnyHeader() + .AllowCredentials() + .SetIsOriginAllowed((host) => true)); + }); + + builder.Services.AddSignalR(); + + return builder; + } + + + internal static CorrelationContext GetCorrelationContext(this IHttpContextAccessor accessor) + => accessor.HttpContext?.Request.Headers.TryGetValue("Correlation-Context", out var json) is true + ? JsonConvert.DeserializeObject(json.FirstOrDefault()) + : null; + + internal static IDictionary GetHeadersToForward(this IMessageProperties messageProperties) + { + const string sagaHeader = "Saga"; + if (messageProperties?.Headers is null || !messageProperties.Headers.TryGetValue(sagaHeader, out var saga)) + { + return null; + } + + return saga is null + ? null + : new Dictionary + { + [sagaHeader] = saga + }; + } + + internal static string GetSpanContext(this IMessageProperties messageProperties, string header) + { + if (messageProperties is null) + { + return string.Empty; + } + + if (messageProperties.Headers.TryGetValue(header, out var span) && span is byte[] spanBytes) + { + return Encoding.UTF8.GetString(spanBytes); + } + + return string.Empty; + } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/IAppContextFactory.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/IAppContextFactory.cs new file mode 100644 index 000000000..64a4521dc --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/IAppContextFactory.cs @@ -0,0 +1,9 @@ +using MiniSpace.Services.Communication.Application; + +namespace MiniSpace.Services.Communication.Infrastructure +{ + public interface IAppContextFactory + { + IAppContext Create(); + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Logging/Extensions.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Logging/Extensions.cs new file mode 100644 index 000000000..ef9726c4f --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Logging/Extensions.cs @@ -0,0 +1,22 @@ +using Convey; +using Convey.Logging.CQRS; +using Microsoft.Extensions.DependencyInjection; +using MiniSpace.Services.Communication.Application.Commands; +using System.Reflection; + +namespace MiniSpace.Services.Communication.Infrastructure.Logging +{ + internal static class Extensions + { + public static IConveyBuilder AddHandlersLogging(this IConveyBuilder builder) + { + var assembly = typeof(UpdateMessageStatus).Assembly; + + builder.Services.AddSingleton(); + + return builder + .AddCommandHandlersLogging(assembly) + .AddEventHandlersLogging(assembly); + } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Logging/MessageToLogTemplateMapper.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Logging/MessageToLogTemplateMapper.cs new file mode 100644 index 000000000..bfe40da1b --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Logging/MessageToLogTemplateMapper.cs @@ -0,0 +1,77 @@ +using Convey.Logging.CQRS; +using Microsoft.Extensions.Logging; +using MiniSpace.Services.Communication.Application.Commands; +using System; +using System.Collections.Generic; + +namespace MiniSpace.Services.Communication.Infrastructure.Logging +{ + internal sealed class MessageToLogTemplateMapper : IMessageToLogTemplateMapper + { + private readonly ILogger _logger; + + private static IReadOnlyDictionary MessageTemplates => new Dictionary + { + { + typeof(AddUserToChat), new HandlerLogTemplate + { + After = "Added user with id: {UserId} to chat with id: {ChatId}." + } + }, + { + typeof(CreateChat), new HandlerLogTemplate + { + After = "Created chat with id: {ChatId}, participants: {ParticipantIds}, and name: '{ChatName}'." + } + }, + { + typeof(DeleteChat), new HandlerLogTemplate + { + After = "Deleted chat with id: {ChatId}." + } + }, + { + typeof(DeleteMessage), new HandlerLogTemplate + { + After = "Deleted message with id: {MessageId} from chat with id: {ChatId}." + } + }, + { + typeof(RemoveUserFromChat), new HandlerLogTemplate + { + After = "Removed user with id: {UserId} from chat with id: {ChatId}." + } + }, + { + typeof(SendMessage), new HandlerLogTemplate + { + After = "Sent message in chat with id: {ChatId} by user with id: {SenderId}, content: '{Content}', and type: '{MessageType}'." + } + }, + { + typeof(UpdateMessageStatus), new HandlerLogTemplate + { + After = "Updated message status to '{Status}' for message with id: {MessageId} in chat with id: {ChatId}." + } + } + }; + + public MessageToLogTemplateMapper(ILogger logger) + { + _logger = logger; + } + + public HandlerLogTemplate Map(TMessage message) where TMessage : class + { + var messageType = message.GetType(); + _logger.LogInformation($"Attempting to map message type: {messageType.Name}"); + if (MessageTemplates.TryGetValue(messageType, out var template)) + { + _logger.LogInformation($"Mapping found. Template: {template.After}"); + return template; + } + _logger.LogWarning($"No mapping found for message type: {messageType.Name}"); + return null; + } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/MiniSpace.Services.Communication.Infrastructure.csproj b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/MiniSpace.Services.Communication.Infrastructure.csproj new file mode 100644 index 000000000..4992a6cf3 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/MiniSpace.Services.Communication.Infrastructure.csproj @@ -0,0 +1,40 @@ + + + + net8.0 + enable + disable + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/MiniSpace.Services.Communication.Infrastructure.sln b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/MiniSpace.Services.Communication.Infrastructure.sln new file mode 100644 index 000000000..4024f4b05 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/MiniSpace.Services.Communication.Infrastructure.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.002.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MiniSpace.Services.Communication.Infrastructure", "MiniSpace.Services.Communication.Infrastructure.csproj", "{994B662D-5C21-4077-8970-C61A227B46D4}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {994B662D-5C21-4077-8970-C61A227B46D4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {994B662D-5C21-4077-8970-C61A227B46D4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {994B662D-5C21-4077-8970-C61A227B46D4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {994B662D-5C21-4077-8970-C61A227B46D4}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {ADCE09CD-5371-42F5-9AB2-04FCDA89D8EA} + EndGlobalSection +EndGlobal diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Mongo/Documents/ChatDocument.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Mongo/Documents/ChatDocument.cs new file mode 100644 index 000000000..98bf65ee4 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Mongo/Documents/ChatDocument.cs @@ -0,0 +1,25 @@ +using Convey.Types; +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; +using System; +using System.Collections.Generic; + +namespace MiniSpace.Services.Communication.Infrastructure.Mongo.Documents +{ + public class ChatDocument : IIdentifiable + { + [BsonId] + [BsonRepresentation(BsonType.String)] + public Guid Id { get; set; } + + public List ParticipantIds { get; set; } + + public List Messages { get; set; } + + public ChatDocument() + { + ParticipantIds = new List(); + Messages = new List(); + } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Mongo/Documents/Extensions.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Mongo/Documents/Extensions.cs new file mode 100644 index 000000000..c82c4f595 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Mongo/Documents/Extensions.cs @@ -0,0 +1,84 @@ +using MiniSpace.Services.Communication.Application.Dto; +using MiniSpace.Services.Communication.Core.Entities; +using MiniSpace.Services.Communication.Infrastructure.Mongo.Documents; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace MiniSpace.Services.Communication.Infrastructure.Mongo.Documents +{ + public static class Extensions +{ + // Converts MessageDocument to Message entity + public static Message AsEntity(this MessageDocument document) + { + // Use the existing ID and other properties directly from the document + return new Message( + document.Id, // Use the existing ID from the document + document.ChatId, + document.SenderId, + document.ReceiverId, + document.Content, + document.Timestamp, + document.Type, + document.Status // Ensure the status is loaded correctly + ); + } + + public static MessageDocument AsDocument(this Message entity) + { + return new MessageDocument + { + Id = entity.Id, + ChatId = entity.ChatId, + SenderId = entity.SenderId, + ReceiverId = entity.ReceiverId, + Content = entity.Content, + Timestamp = entity.Timestamp, + Type = entity.Type, + Status = entity.Status + }; + } + + public static Chat AsEntity(this ChatDocument document) + { + var messages = document.Messages.Select(m => m.AsEntity()).ToList(); + return new Chat(document.Id, document.ParticipantIds, messages); + } + + public static ChatDocument AsDocument(this Chat entity) + { + return new ChatDocument + { + Id = entity.Id, + ParticipantIds = entity.ParticipantIds, + Messages = entity.Messages.Select(m => m.AsDocument()).ToList() + }; + } + + public static ChatDto AsDto(this Chat entity) + { + return new ChatDto + { + Id = entity.Id, + ParticipantIds = entity.ParticipantIds, + Messages = entity.Messages.Select(m => m.AsDto()).ToList() + }; + } + + public static MessageDto AsDto(this Message entity) + { + return new MessageDto + { + Id = entity.Id, // Ensure the ID is correctly mapped + ChatId = entity.ChatId, + SenderId = entity.SenderId, + Content = entity.Content, + Timestamp = entity.Timestamp, + MessageType = entity.Type.ToString(), + Status = entity.Status.ToString() + }; + } +} + + } diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Mongo/Documents/MessageDocument.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Mongo/Documents/MessageDocument.cs new file mode 100644 index 000000000..24ecd3022 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Mongo/Documents/MessageDocument.cs @@ -0,0 +1,22 @@ +using Convey.Types; +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; +using MiniSpace.Services.Communication.Core.Entities; +using System; + +namespace MiniSpace.Services.Communication.Infrastructure.Mongo.Documents +{ + public class MessageDocument : IIdentifiable + { + [BsonId] + [BsonRepresentation(BsonType.String)] + public Guid Id { get; set; } + public Guid ChatId { get; set; } + public Guid SenderId { get; set; } + public Guid ReceiverId { get; set; } + public string Content { get; set; } + public DateTime Timestamp { get; set; } + public MessageType Type { get; set; } + public MessageStatus Status { get; set; } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Mongo/Documents/OrganizationChatsDocument.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Mongo/Documents/OrganizationChatsDocument.cs new file mode 100644 index 000000000..f65e70d73 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Mongo/Documents/OrganizationChatsDocument.cs @@ -0,0 +1,21 @@ +using Convey.Types; +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; +using System; +using System.Collections.Generic; + +namespace MiniSpace.Services.Communication.Infrastructure.Mongo.Documents +{ + public class OrganizationChatsDocument : IIdentifiable + { + [BsonId] + [BsonRepresentation(BsonType.String)] + public Guid Id { get; set; } + public Guid OrganizationId { get; set; } + public List Chats { get; set; } + public OrganizationChatsDocument() + { + Chats = new List(); + } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Mongo/Documents/OrganizationChatsExtensions.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Mongo/Documents/OrganizationChatsExtensions.cs new file mode 100644 index 000000000..7be5c2722 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Mongo/Documents/OrganizationChatsExtensions.cs @@ -0,0 +1,32 @@ +using MiniSpace.Services.Communication.Core.Entities; +using MiniSpace.Services.Communication.Infrastructure.Mongo.Documents; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace MiniSpace.Services.Communication.Infrastructure.Mongo.Documents +{ + public static class OrganizationChatsExtensions + { + public static OrganizationChats AsEntity(this OrganizationChatsDocument document) + { + var organizationChats = new OrganizationChats(document.OrganizationId); + foreach (var chatDocument in document.Chats) + { + var chat = chatDocument.AsEntity(); + organizationChats.AddChat(chat); + } + return organizationChats; + } + + public static OrganizationChatsDocument AsDocument(this OrganizationChats entity) + { + return new OrganizationChatsDocument + { + Id = Guid.NewGuid(), + OrganizationId = entity.OrganizationId, + Chats = entity.Chats.Select(c => c.AsDocument()).ToList() + }; + } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Mongo/Documents/UserChatsDocument.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Mongo/Documents/UserChatsDocument.cs new file mode 100644 index 000000000..8e39b3f25 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Mongo/Documents/UserChatsDocument.cs @@ -0,0 +1,21 @@ +using Convey.Types; +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; +using System; +using System.Collections.Generic; + +namespace MiniSpace.Services.Communication.Infrastructure.Mongo.Documents +{ + public class UserChatsDocument : IIdentifiable + { + [BsonId] + [BsonRepresentation(BsonType.String)] + public Guid Id { get; set; } + public Guid UserId { get; set; } + public List Chats { get; set; } + public UserChatsDocument() + { + Chats = new List(); + } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Mongo/Documents/UserChatsExtensions.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Mongo/Documents/UserChatsExtensions.cs new file mode 100644 index 000000000..a1a702e2b --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Mongo/Documents/UserChatsExtensions.cs @@ -0,0 +1,32 @@ +using MiniSpace.Services.Communication.Core.Entities; +using MiniSpace.Services.Communication.Infrastructure.Mongo.Documents; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace MiniSpace.Services.Communication.Infrastructure.Mongo.Documents +{ + public static class UserChatsExtensions + { + public static UserChats AsEntity(this UserChatsDocument document) + { + var userChats = new UserChats(document.UserId); + foreach (var chatDocument in document.Chats) + { + var chat = chatDocument.AsEntity(); + userChats.AddChat(chat); + } + return userChats; + } + + public static UserChatsDocument AsDocument(this UserChats entity) + { + return new UserChatsDocument + { + Id = Guid.NewGuid(), + UserId = entity.UserId, + Chats = entity.Chats.Select(c => c.AsDocument()).ToList() + }; + } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Mongo/Queries/Handlers/GetChatByIdHandler.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Mongo/Queries/Handlers/GetChatByIdHandler.cs new file mode 100644 index 000000000..bd7bb4376 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Mongo/Queries/Handlers/GetChatByIdHandler.cs @@ -0,0 +1,29 @@ +using System.Threading.Tasks; +using Convey.CQRS.Queries; +using MiniSpace.Services.Communication.Application.Dto; +using MiniSpace.Services.Communication.Application.Queries; +using MiniSpace.Services.Communication.Core.Repositories; +using MiniSpace.Services.Communication.Infrastructure.Mongo.Documents; + +namespace MiniSpace.Services.Communication.Infrastructure.Mongo.Queries.Handlers +{ + public class GetChatByIdHandler : IQueryHandler + { + private readonly IUserChatsRepository _userChatsRepository; + private readonly IOrganizationChatsRepository _organizationChatsRepository; + + public GetChatByIdHandler(IUserChatsRepository userChatsRepository, IOrganizationChatsRepository organizationChatsRepository) + { + _userChatsRepository = userChatsRepository; + _organizationChatsRepository = organizationChatsRepository; + } + + public async Task HandleAsync(GetChatById query, CancellationToken cancellationToken) + { + var userChat = await _userChatsRepository.GetByUserIdAsync(query.ChatId); + var chat = userChat?.GetChatById(query.ChatId) ?? (await _organizationChatsRepository.GetByOrganizationIdAsync(query.ChatId))?.GetChatById(query.ChatId); + + return chat?.AsDocument().AsEntity().AsDto(); + } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Mongo/Queries/Handlers/GetMessagesForChatHandler.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Mongo/Queries/Handlers/GetMessagesForChatHandler.cs new file mode 100644 index 000000000..84c91211f --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Mongo/Queries/Handlers/GetMessagesForChatHandler.cs @@ -0,0 +1,37 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Convey.CQRS.Queries; +using MiniSpace.Services.Communication.Application.Dto; +using MiniSpace.Services.Communication.Application.Queries; +using MiniSpace.Services.Communication.Core.Repositories; +using MiniSpace.Services.Communication.Infrastructure.Mongo.Documents; + + +namespace MiniSpace.Services.Communication.Infrastructure.Mongo.Queries.Handlers +{ + public class GetMessagesForChatHandler : IQueryHandler> + { + private readonly IUserChatsRepository _userChatsRepository; + + public GetMessagesForChatHandler(IUserChatsRepository userChatsRepository) + { + _userChatsRepository = userChatsRepository; + } + + public async Task> HandleAsync(GetMessagesForChat query, CancellationToken cancellationToken) + { + var chat = await _userChatsRepository.GetByChatIdAsync(query.ChatId); + + if (chat != null) + { + var messages = chat.Messages.Select(m => m.AsDto()).ToList(); + return messages; + } + + return Enumerable.Empty(); + } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Mongo/Queries/Handlers/GetUserChatsHandler.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Mongo/Queries/Handlers/GetUserChatsHandler.cs new file mode 100644 index 000000000..aa0ede7b9 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Mongo/Queries/Handlers/GetUserChatsHandler.cs @@ -0,0 +1,45 @@ +using System.Linq; +using System.Threading.Tasks; +using Convey.CQRS.Queries; +using MiniSpace.Services.Communication.Infrastructure.Mongo.Documents; +using MiniSpace.Services.Communication.Application.Dto; +using MiniSpace.Services.Communication.Application.Queries; +using MiniSpace.Services.Communication.Core.Repositories; +using MiniSpace.Services.Communication.Core.Wrappers; + +namespace MiniSpace.Services.Communication.Infrastructure.Mongo.Queries.Handlers +{ + public class GetUserChatsHandler : IQueryHandler> + { + private readonly IUserChatsRepository _userChatsRepository; + + public GetUserChatsHandler(IUserChatsRepository userChatsRepository) + { + _userChatsRepository = userChatsRepository; + } + + public async Task> HandleAsync(GetUserChats query, CancellationToken cancellationToken) + { + var userChats = await _userChatsRepository.GetByUserIdAsync(query.UserId); + + if (userChats == null || !userChats.Chats.Any()) + { + return new PagedResponse(Enumerable.Empty(), 0, query.PageSize, 0); + } + + var paginatedChats = userChats.Chats + .Skip((query.Page - 1) * query.PageSize) + .Take(query.PageSize) + .Select(chat => chat.AsDto()) + .ToList(); + + var userChatDto = new UserChatDto + { + UserId = query.UserId, + Chats = paginatedChats + }; + + return new PagedResponse(new List { userChatDto }, query.Page, query.PageSize, userChats.Chats.Count); + } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Mongo/Repositories/OrganizationChatsRepository.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Mongo/Repositories/OrganizationChatsRepository.cs new file mode 100644 index 000000000..909cd3a47 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Mongo/Repositories/OrganizationChatsRepository.cs @@ -0,0 +1,81 @@ +using MiniSpace.Services.Communication.Core.Entities; +using MiniSpace.Services.Communication.Core.Repositories; +using MiniSpace.Services.Communication.Infrastructure.Mongo.Documents; +using MongoDB.Driver; +using Convey.Persistence.MongoDB; +using System; +using System.Threading.Tasks; + +namespace MiniSpace.Services.Communication.Infrastructure.Mongo.Repositories +{ + public class OrganizationChatsRepository : IOrganizationChatsRepository + { + private readonly IMongoRepository _repository; + + public OrganizationChatsRepository(IMongoRepository repository) + { + _repository = repository; + } + + public async Task GetByOrganizationIdAsync(Guid organizationId) + { + var document = await _repository.GetAsync(x => x.OrganizationId == organizationId); + return document?.AsEntity(); + } + + public async Task AddAsync(OrganizationChats organizationChats) + { + await _repository.AddAsync(organizationChats.AsDocument()); + } + + public async Task UpdateAsync(OrganizationChats organizationChats) + { + await _repository.UpdateAsync(organizationChats.AsDocument()); + } + + public async Task AddOrUpdateAsync(OrganizationChats organizationChats) + { + var existingDocument = await _repository.GetAsync(x => x.OrganizationId == organizationChats.OrganizationId); + if (existingDocument == null) + { + await AddAsync(organizationChats); + } + else + { + await UpdateAsync(organizationChats); + } + } + + public async Task DeleteAsync(Guid organizationId) + { + await _repository.DeleteAsync(x => x.OrganizationId == organizationId); + } + + public async Task ChatExistsAsync(Guid organizationId, Guid chatId) + { + var document = await _repository.GetAsync(x => x.OrganizationId == organizationId && x.Chats.Any(c => c.Id == chatId)); + return document != null; + } + + public async Task AddChatAsync(Guid organizationId, Chat chat) + { + var organizationChats = await GetByOrganizationIdAsync(organizationId) ?? new OrganizationChats(organizationId); + organizationChats.AddChat(chat); + await AddOrUpdateAsync(organizationChats); + } + + public async Task DeleteChatAsync(Guid organizationId, Guid chatId) + { + var organizationChats = await GetByOrganizationIdAsync(organizationId); + if (organizationChats != null) + { + var chat = organizationChats.GetChatById(chatId); + if (chat != null) + { + organizationChats.Chats.Remove(chat); + await UpdateAsync(organizationChats); + } + } + } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Mongo/Repositories/UserChatsRepository.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Mongo/Repositories/UserChatsRepository.cs new file mode 100644 index 000000000..1db100707 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Mongo/Repositories/UserChatsRepository.cs @@ -0,0 +1,106 @@ +using MiniSpace.Services.Communication.Core.Entities; +using MiniSpace.Services.Communication.Core.Repositories; +using MiniSpace.Services.Communication.Infrastructure.Mongo.Documents; +using MongoDB.Driver; +using Convey.Persistence.MongoDB; +using System; +using System.Linq; +using System.Threading.Tasks; + +namespace MiniSpace.Services.Communication.Infrastructure.Mongo.Repositories +{ + public class UserChatsRepository : IUserChatsRepository + { + private readonly IMongoRepository _repository; + + public UserChatsRepository(IMongoRepository repository) + { + _repository = repository; + } + + public async Task GetByUserIdAsync(Guid userId) + { + var document = await _repository.GetAsync(x => x.UserId == userId); + return document?.AsEntity(); + } + + public async Task GetByChatIdAsync(Guid chatId) + { + var document = await _repository.Collection.Find(x => x.Chats.Any(c => c.Id == chatId)).FirstOrDefaultAsync(); + var chatDocument = document?.Chats.FirstOrDefault(c => c.Id == chatId); + return chatDocument?.AsEntity(); + } + + public async Task AddAsync(UserChats userChats) + { + await _repository.AddAsync(userChats.AsDocument()); + } + + public async Task UpdateAsync(UserChats userChats) + { + var filter = Builders.Filter.Eq(doc => doc.UserId, userChats.UserId); + var update = Builders.Update + .Set(doc => doc.Chats, userChats.Chats.Select(chat => chat.AsDocument()).ToList()); + + await _repository.Collection.UpdateOneAsync(filter, update); + } + + public async Task AddOrUpdateAsync(UserChats userChats) + { + var existingDocument = await _repository.GetAsync(x => x.UserId == userChats.UserId); + if (existingDocument == null) + { + await AddAsync(userChats); + } + else + { + await UpdateAsync(userChats); + } + } + + public async Task DeleteAsync(Guid userId) + { + await _repository.DeleteAsync(x => x.UserId == userId); + } + + public async Task ChatExistsAsync(Guid userId, Guid chatId) + { + var document = await _repository.GetAsync(x => x.UserId == userId && x.Chats.Any(c => c.Id == chatId)); + return document != null; + } + + public async Task AddChatAsync(Guid userId, Chat chat) + { + var userChats = await GetByUserIdAsync(userId) ?? new UserChats(userId); + userChats.AddChat(chat); + await AddOrUpdateAsync(userChats); + } + + public async Task DeleteChatAsync(Guid userId, Guid chatId) + { + var userChats = await GetByUserIdAsync(userId); + if (userChats == null) + { + return; + } + var chat = userChats.GetChatById(chatId); + if (chat == null) + { + return; + } + userChats.Chats.Remove(chat); + await UpdateAsync(userChats); + } + + public async Task> GetParticipantIdsByChatIdAsync(Guid chatId) + { + var document = await _repository.Collection + .Find(x => x.Chats.Any(c => c.Id == chatId)) + .FirstOrDefaultAsync(); + + var chatDocument = document?.Chats.FirstOrDefault(c => c.Id == chatId); + return chatDocument?.ParticipantIds ?? new List(); + } + + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Services/Clients/StudentsServiceClient.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Services/Clients/StudentsServiceClient.cs new file mode 100644 index 000000000..2c20e4f2b --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Services/Clients/StudentsServiceClient.cs @@ -0,0 +1,25 @@ +using System; +using System.Text.Json; +using System.Threading.Tasks; +using Convey.HTTP; +using MiniSpace.Services.Communication.Application.Dto; +using MiniSpace.Services.Communication.Application.Queries; +using MiniSpace.Services.Communication.Application.Services.Clients; + +namespace MiniSpace.Services.Communication.Infrastructure.Services.Clients +{ + public class StudentsServiceClient : IStudentsServiceClient + { + private readonly IHttpClient _httpClient; + private readonly string _url; + + public StudentsServiceClient(IHttpClient httpClient, HttpClientOptions options) + { + _httpClient = httpClient; + _url = options.Services["students"]; + } + + public Task GetAsync(Guid id) + => _httpClient.GetAsync($"{_url}/students/{id}"); + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Services/DateTimeProvider.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Services/DateTimeProvider.cs new file mode 100644 index 000000000..692cc86f0 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Services/DateTimeProvider.cs @@ -0,0 +1,9 @@ +using MiniSpace.Services.Communication.Application.Services; + +namespace MiniSpace.Services.Communication.Infrastructure.Services +{ + internal sealed class DateTimeProvider : IDateTimeProvider + { + public DateTime Now => DateTime.UtcNow; + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Services/EventMapper.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Services/EventMapper.cs new file mode 100644 index 000000000..cfbb8a5b6 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Services/EventMapper.cs @@ -0,0 +1,32 @@ +using Convey.CQRS.Events; +using MiniSpace.Services.Communication.Application.Events; +using MiniSpace.Services.Communication.Application.Services; +using MiniSpace.Services.Communication.Core.Events; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace MiniSpace.Services.Communication.Infrastructure.Services +{ + public class EventMapper : IEventMapper + { + public IEnumerable MapAll(IEnumerable events) + => events.Select(Map).Where(mappedEvent => mappedEvent != null); + + public IEvent Map(IDomainEvent @event) + { + switch (@event) + { + case MessageAddedEvent e: + return new MessageSent(e.ChatId, e.MessageId, Guid.Empty, string.Empty); + + // Add more cases for other domain events + // case SomeOtherDomainEvent e: + // return new SomeOtherIntegrationEvent(...); + + default: + return null; + } + } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Services/MessageBroker.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Services/MessageBroker.cs new file mode 100644 index 000000000..4bb6cbdd4 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Services/MessageBroker.cs @@ -0,0 +1,94 @@ +using Convey.CQRS.Events; +using Convey.MessageBrokers; +using Convey.MessageBrokers.Outbox; +using Convey.MessageBrokers.RabbitMQ; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using OpenTracing; +using MiniSpace.Services.Communication.Application.Services; +using MiniSpace.Services.Communication.Infrastructure; +using System.Text.Json; + +namespace MiniSpace.Services.Communication.Infrastructure.Services +{ + internal sealed class MessageBroker : IMessageBroker + { + private const string DefaultSpanContextHeader = "span_context"; + private readonly IBusPublisher _busPublisher; + private readonly IMessageOutbox _outbox; + private readonly ICorrelationContextAccessor _contextAccessor; + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly IMessagePropertiesAccessor _messagePropertiesAccessor; + private readonly ITracer _tracer; + private readonly ILogger _logger; + private readonly string _spanContextHeader; + + public MessageBroker(IBusPublisher busPublisher, IMessageOutbox outbox, + ICorrelationContextAccessor contextAccessor, IHttpContextAccessor httpContextAccessor, + IMessagePropertiesAccessor messagePropertiesAccessor, RabbitMqOptions options, ITracer tracer, + ILogger logger) + { + _busPublisher = busPublisher; + _outbox = outbox; + _contextAccessor = contextAccessor; + _httpContextAccessor = httpContextAccessor; + _messagePropertiesAccessor = messagePropertiesAccessor; + _tracer = tracer; + _logger = logger; + _spanContextHeader = string.IsNullOrWhiteSpace(options.SpanContextHeader) + ? DefaultSpanContextHeader + : options.SpanContextHeader; + } + + public Task PublishAsync(params IEvent[] events) => PublishAsync(events?.AsEnumerable()); + + public async Task PublishAsync(IEnumerable events) + { + if (events is null) + { + return; + } + + var messageProperties = _messagePropertiesAccessor.MessageProperties; + var originatedMessageId = messageProperties?.MessageId; + var correlationId = messageProperties?.CorrelationId; + var spanContext = messageProperties?.GetSpanContext(_spanContextHeader); + if (string.IsNullOrWhiteSpace(spanContext)) + { + spanContext = _tracer.ActiveSpan is null ? string.Empty : _tracer.ActiveSpan.Context.ToString(); + } + + var headers = messageProperties.GetHeadersToForward(); + var correlationContext = _contextAccessor.CorrelationContext ?? + _httpContextAccessor.GetCorrelationContext(); + + foreach (var @event in events) + { + if (@event is null) + { + continue; + } + + var messageId = Guid.NewGuid().ToString("N"); + _logger.LogTrace($"Publishing integration event: {@event.GetType().Name} [id: '{messageId}']."); + var serializedEvent = JsonSerializer.Serialize(@event, new JsonSerializerOptions + { + WriteIndented = true, // To make it easier to read in logs + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }); + + _logger.LogTrace($"Publishing integration event: {@event.GetType().Name} [id: '{messageId}'], Content: {serializedEvent}"); + // Console.WriteLine($"Publishing Event: {@event.GetType().Name} with ID: {messageId}, Content: {serializedEvent}"); + if (_outbox.Enabled) + { + await _outbox.SendAsync(@event, originatedMessageId, messageId, correlationId, spanContext, + correlationContext, headers); + continue; + } + + await _busPublisher.PublishAsync(@event, messageId, correlationId, spanContext, correlationContext, + headers); + } + } + } +} diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Api/Program.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Api/Program.cs index 802da81d1..42c9125f5 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Api/Program.cs +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Api/Program.cs @@ -17,6 +17,7 @@ using MiniSpace.Services.Events.Application.Services; using MiniSpace.Services.Events.Application.Wrappers; using MiniSpace.Services.Events.Infrastructure; +using MiniSpace.Services.Events.Core.Wrappers; using System; using System.IO; using Microsoft.AspNetCore.Builder; @@ -36,16 +37,9 @@ public static async Task Main(string[] args) .AddInfrastructure() .Build()) .Configure(app => app - // .UseMiddleware() - // .UseMiddleware() .UseInfrastructure() .UseEndpoints(endpoints => endpoints - .Get("", ctx => ctx.Response.WriteAsync(ctx.RequestServices.GetService().Name)) - // .Post("events/search", async (cmd, ctx) => - // { - // var pagedResult = await ctx.RequestServices.GetService().BrowseEventsAsync(cmd); - // await ctx.Response.WriteJsonAsync(pagedResult); - // }) + .Get("", ctx => ctx.Response.WriteAsync(ctx.RequestServices.GetService().Name)) .Post("events/search/organizer", async (cmd, ctx) => { var pagedResult = await ctx.RequestServices.GetService().BrowseOrganizerEventsAsync(cmd); @@ -53,13 +47,14 @@ public static async Task Main(string[] args) })) .UseDispatcherEndpoints(endpoints => endpoints .Get("events/{eventId}") - .Get>("events/users/{userId}") + .Get>("events/users/{userId}") .Get("events/{eventId}/participants") .Get("events/{eventId}/rating") - .Get>("events/paginated") - .Get>("events/organizer/{organizerId}/paginated") - .Get>("events/search") - + .Get>("events/paginated") + .Get>("events/organizer/{organizerId}/paginated") + .Get>("events/search") + .Get>("events/users/{userId}/feed") + .Get>("events/users/{userId}/views/paginated") .Put("events/{eventId}") .Post("events", afterDispatch: (cmd, ctx) => ctx.Response.Created($"events/{cmd.EventId}")) @@ -71,6 +66,7 @@ public static async Task Main(string[] args) .Post("events/{eventId}/rate") .Delete("events/{eventId}/rate") .Post("events/{eventId}/participants") + .Post("events/{eventId}/view") .Delete("events/{eventId}/participants") ) ) @@ -78,75 +74,4 @@ public static async Task Main(string[] args) .Build() .RunAsync(); } - - public class RequestLoggingMiddleware - { - private readonly RequestDelegate _next; - - public RequestLoggingMiddleware(RequestDelegate next) - { - _next = next; - } - - public async Task InvokeAsync(HttpContext context) - { - // Enable buffering so the stream can be read multiple times - context.Request.EnableBuffering(); - - // Read the stream as text - var bodyAsText = await new StreamReader(context.Request.Body).ReadToEndAsync(); - - // Log the request body - Console.WriteLine("Received JSON:"); - Console.WriteLine(bodyAsText); - - // Reset the stream position to allow the next middleware to read it - context.Request.Body.Position = 0; - - // Call the next middleware in the pipeline - await _next(context); - } - } - - public class ExceptionHandlingMiddleware -{ - private readonly RequestDelegate _next; - private readonly ILogger _logger; - - public ExceptionHandlingMiddleware(RequestDelegate next, ILogger logger) - { - _next = next; - _logger = logger; - } - - public async Task InvokeAsync(HttpContext context) - { - try - { - await _next(context); - } - catch (Exception ex) - { - _logger.LogError(ex, "An unhandled exception occurred."); - await HandleExceptionAsync(context, ex); - } - } - - private static Task HandleExceptionAsync(HttpContext context, Exception exception) - { - var statusCode = StatusCodes.Status500InternalServerError; - var result = JsonSerializer.Serialize(new { error = exception.Message }); - - if (exception is ArgumentException || exception is InvalidOperationException) - { - statusCode = StatusCodes.Status400BadRequest; - } - - context.Response.ContentType = "application/json"; - context.Response.StatusCode = statusCode; - return context.Response.WriteAsync(result); - } -} - - } diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/Handlers/ViewEventHandler.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/Handlers/ViewEventHandler.cs new file mode 100644 index 000000000..a99f9cff2 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/Handlers/ViewEventHandler.cs @@ -0,0 +1,63 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Convey.CQRS.Commands; +using MiniSpace.Services.Events.Core.Entities; +using MiniSpace.Services.Events.Core.Repositories; +using Microsoft.Extensions.Logging; + +namespace MiniSpace.Services.Events.Application.Commands.Handlers +{ + public class ViewEventHandler : ICommandHandler + { + private readonly IEventsUserViewsRepository _eventsUserViewsRepository; + private readonly IEventRepository _eventRepository; + private readonly ILogger _logger; + + public ViewEventHandler( + IEventsUserViewsRepository eventsUserViewsRepository, + IEventRepository eventRepository, + ILogger logger) + { + _eventsUserViewsRepository = eventsUserViewsRepository; + _eventRepository = eventRepository; + _logger = logger; + } + + public async Task HandleAsync(ViewEvent command, CancellationToken cancellationToken) + { + // Ensure the event exists + var eventExists = await _eventRepository.ExistsAsync(command.EventId); + if (!eventExists) + { + _logger.LogWarning($"Event with ID {command.EventId} not found."); + return; + } + + // Fetch the user's event views + var userViews = await _eventsUserViewsRepository.GetAsync(command.UserId); + if (userViews == null) + { + // If no views exist, create a new EventsViews object for the user + userViews = new EventsViews(command.UserId, Enumerable.Empty()); + } + + // Check if the event has already been viewed + var existingView = userViews.Views.FirstOrDefault(v => v.EventId == command.EventId); + if (existingView != null) + { + // Remove the existing view (to update the date) + userViews.RemoveView(command.EventId); + } + + // Add the new view with the current date + userViews.AddView(command.EventId, DateTime.UtcNow); + + // Save the updated views to the repository + await _eventsUserViewsRepository.UpdateAsync(userViews); + + _logger.LogInformation($"User {command.UserId} viewed event {command.EventId}."); + } + } +} diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/ViewEvent.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/ViewEvent.cs new file mode 100644 index 000000000..d1a668dea --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/ViewEvent.cs @@ -0,0 +1,17 @@ +using System; +using Convey.CQRS.Commands; + +namespace MiniSpace.Services.Events.Application.Commands +{ + public class ViewEvent : ICommand + { + public Guid UserId { get; } + public Guid EventId { get; } + + public ViewEvent(Guid userId, Guid eventId) + { + UserId = userId; + EventId = eventId; + } + } +} diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/DTO/EducationDto.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/DTO/EducationDto.cs new file mode 100644 index 000000000..26ce34bd1 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/DTO/EducationDto.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace MiniSpace.Services.Events.Application.DTO +{ + [ExcludeFromCodeCoverage] + public class EducationDto + { + public string InstitutionName { get; set; } + public string Degree { get; set; } + public DateTime StartDate { get; set; } + public DateTime EndDate { get; set; } + public string Description { get; set; } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/DTO/FriendDto.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/DTO/FriendDto.cs index 23aeb3968..4daa0d38e 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/DTO/FriendDto.cs +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/DTO/FriendDto.cs @@ -7,9 +7,9 @@ namespace MiniSpace.Services.Events.Application.DTO public class FriendDto { public Guid Id { get; set; } - public Guid StudentId { get; set; } + public Guid UserId { get; set; } public Guid FriendId { get; set; } public DateTime CreatedAt { get; set; } - public string FriendState { get; set; } + public string State { get; set; } } } \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/DTO/PagedResult.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/DTO/PagedResult.cs deleted file mode 100644 index cad6a334d..000000000 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/DTO/PagedResult.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace MiniSpace.Services.Events.Application.DTO -{ - public class PagedResult - { - public IEnumerable Items { get; } - public int Page { get; } - public int PageSize { get; } - public int TotalItems { get; } - public int TotalPages => PageSize > 0 ? (int)Math.Ceiling((decimal)TotalItems / PageSize) : 0; - - public int? NextPage => Page < TotalPages ? Page + 1 : (int?)null; - public int? PreviousPage => Page > 1 ? Page - 1 : (int?)null; - - public PagedResult(IEnumerable items, int page, int pageSize, int totalItems) - { - Items = items; - Page = page; - PageSize = pageSize; - TotalItems = totalItems; - } - } -} diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/DTO/StudentDto.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/DTO/StudentDto.cs index ca61ea03c..2e8f31f6f 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/DTO/StudentDto.cs +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/DTO/StudentDto.cs @@ -7,6 +7,5 @@ namespace MiniSpace.Services.Events.Application.DTO public class StudentDto { public Guid Id { get; set; } - public string Name { get; set; } } } \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/DTO/UserEventsViewsDto.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/DTO/UserEventsViewsDto.cs new file mode 100644 index 000000000..47eb5d429 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/DTO/UserEventsViewsDto.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; + +namespace MiniSpace.Services.Events.Application.DTO +{ + public class UserEventsViewsDto + { + public Guid UserId { get; set; } + public IEnumerable Views { get; set; } + } +} diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/DTO/StudentFriendsDto.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/DTO/UserFriendsDto.cs similarity index 77% rename from MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/DTO/StudentFriendsDto.cs rename to MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/DTO/UserFriendsDto.cs index f22288d5b..90765c36d 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/DTO/StudentFriendsDto.cs +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/DTO/UserFriendsDto.cs @@ -5,9 +5,9 @@ namespace MiniSpace.Services.Events.Application.DTO { [ExcludeFromCodeCoverage] - public class StudentFriendsDto + public class UserFriendsDto { - public Guid StudentId { get; set; } + public Guid UserId { get; set; } public List Friends { get; set; } = new List(); } } \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/DTO/UserFromServiceDto.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/DTO/UserFromServiceDto.cs new file mode 100644 index 000000000..55706a5ae --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/DTO/UserFromServiceDto.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + + +namespace MiniSpace.Services.Events.Application.DTO +{ + [ExcludeFromCodeCoverage] + public class UserFromServiceDto + { + public Guid Id { get; set; } + public string Email { get; set; } + public string FirstName { get; set; } + public string LastName { get; set; } + public string ProfileImageUrl { get; set; } + public string Description { get; set; } + public DateTime? DateOfBirth { get; set; } + public bool EmailNotifications { get; set; } + public bool IsBanned { get; set; } + public string State { get; set; } + public DateTime CreatedAt { get; set; } + public string ContactEmail { get; set; } + public string BannerUrl { get; set; } + public string PhoneNumber { get; set; } + public IEnumerable Languages { get; set; } + public IEnumerable Interests { get; set; } + public IEnumerable Education { get; set; } + public IEnumerable Work { get; set; } + public bool IsTwoFactorEnabled { get; set; } + public string TwoFactorSecret { get; set; } + public IEnumerable InterestedInEvents { get; set; } + public IEnumerable SignedUpEvents { get; set; } + public string Country { get; set; } + public string City { get; set; } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/DTO/ViewDto.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/DTO/ViewDto.cs new file mode 100644 index 000000000..618f4a79c --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/DTO/ViewDto.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; + +namespace MiniSpace.Services.Events.Application.DTO +{ + public class ViewDto + { + public Guid EventId { get; set; } + public DateTime Date { get; set; } + } +} diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/DTO/WorkDto.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/DTO/WorkDto.cs new file mode 100644 index 000000000..77b848b25 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/DTO/WorkDto.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace MiniSpace.Services.Events.Application.DTO +{ + [ExcludeFromCodeCoverage] + public class WorkDto + { + public string Company { get; set; } + public string Position { get; set; } + public DateTime StartDate { get; set; } + public DateTime EndDate { get; set; } + public string Description { get; set; } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/External/CommentCreated.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/External/CommentCreated.cs new file mode 100644 index 000000000..521748421 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/External/CommentCreated.cs @@ -0,0 +1,37 @@ +using Convey.CQRS.Events; +using Convey.MessageBrokers; +using System; + +namespace MiniSpace.Services.Events.Application.Events.External +{ + [Message("comments")] + public class CommentCreated : IEvent + { + public Guid CommentId { get; } + public Guid ContextId { get; } + public string CommentContext { get; } + public Guid UserId { get; } + public Guid ParentId { get; } + public string TextContent { get; } + public DateTime CreatedAt { get; } + public DateTime LastUpdatedAt { get; } + public int RepliesCount { get; } + public bool IsDeleted { get; } + + public CommentCreated(Guid commentId, Guid contextId, string commentContext, Guid userId, + Guid parentId, string textContent, DateTime createdAt, + DateTime lastUpdatedAt, int repliesCount, bool isDeleted) + { + CommentId = commentId; + ContextId = contextId; + CommentContext = commentContext; + UserId = userId; + ParentId = parentId; + TextContent = textContent; + CreatedAt = createdAt; + LastUpdatedAt = lastUpdatedAt; + RepliesCount = repliesCount; + IsDeleted = isDeleted; + } + } +} diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/External/Handlers/CommentCreatedHandler.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/External/Handlers/CommentCreatedHandler.cs new file mode 100644 index 000000000..326facb80 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/External/Handlers/CommentCreatedHandler.cs @@ -0,0 +1,47 @@ +using System; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Convey.CQRS.Events; +using MiniSpace.Services.Events.Application.Events.External; +using MiniSpace.Services.Events.Core.Entities; +using MiniSpace.Services.Events.Core.Repositories; + +namespace MiniSpace.Services.Events.Application.Events.External.Handlers +{ + public class CommentCreatedHandler : IEventHandler + { + private readonly IUserCommentsHistoryRepository _userCommentsHistoryRepository; + + public CommentCreatedHandler(IUserCommentsHistoryRepository userCommentsHistoryRepository) + { + _userCommentsHistoryRepository = userCommentsHistoryRepository; + } + + public async Task HandleAsync(CommentCreated @event, CancellationToken cancellationToken = default) + { + + var eventJson = JsonSerializer.Serialize(@event, new JsonSerializerOptions + { + WriteIndented = true // Optional: For pretty-printing the JSON + }); + Console.WriteLine("Received CommentCreated event:"); + Console.WriteLine(eventJson); + + var comment = new Comment( + @event.CommentId, + @event.ContextId, + @event.CommentContext, + @event.UserId, + @event.ParentId, + @event.TextContent, + @event.CreatedAt, + @event.LastUpdatedAt, + @event.RepliesCount, + @event.IsDeleted + ); + + await _userCommentsHistoryRepository.SaveCommentAsync(@event.UserId, comment); + } + } +} diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/External/Handlers/ReactionCreatedHandler.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/External/Handlers/ReactionCreatedHandler.cs new file mode 100644 index 000000000..e4967cbf3 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/External/Handlers/ReactionCreatedHandler.cs @@ -0,0 +1,42 @@ +using System; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Convey.CQRS.Events; +using MiniSpace.Services.Events.Application.Events.External; +using MiniSpace.Services.Events.Core.Entities; +using MiniSpace.Services.Events.Core.Repositories; + +namespace MiniSpace.Services.Events.Application.Events.External.Handlers +{ + public class ReactionCreatedHandler : IEventHandler + { + private readonly IUserReactionsHistoryRepository _userReactionsHistoryRepository; + + public ReactionCreatedHandler(IUserReactionsHistoryRepository userReactionsHistoryRepository) + { + _userReactionsHistoryRepository = userReactionsHistoryRepository; + } + + public async Task HandleAsync(ReactionCreated @event, CancellationToken cancellationToken = default) + { + var eventJson = JsonSerializer.Serialize(@event, new JsonSerializerOptions + { + WriteIndented = true + }); + Console.WriteLine("Received ReactionCreated event:"); + Console.WriteLine(eventJson); + + var reaction = Reaction.Create( + @event.ReactionId, + @event.UserId, + @event.ReactionType, + @event.ContentId, + @event.ContentType, + @event.TargetType + ); + + await _userReactionsHistoryRepository.SaveReactionAsync(@event.UserId, reaction); + } + } +} diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/External/ReactionCreated.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/External/ReactionCreated.cs new file mode 100644 index 000000000..0b4b0dfa1 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/External/ReactionCreated.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Convey.CQRS.Events; +using Convey.MessageBrokers; + +namespace MiniSpace.Services.Events.Application.Events.External +{ + [Message("reactions")] + public class ReactionCreated : IEvent + { + public Guid ReactionId { get; } + public Guid UserId { get; } + public Guid ContentId { get; } + public string ContentType { get; } + public string ReactionType { get; } + public string TargetType { get; } + + public ReactionCreated(Guid reactionId, Guid userId, Guid contentId, string contentType, string reactionType, string targetType) + { + ReactionId = reactionId; + UserId = userId; + ContentId = contentId; + ContentType = contentType; + ReactionType = reactionType; + TargetType = targetType; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Queries/GetPaginatedEvents.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Queries/GetPaginatedEvents.cs index 84b4ae86c..03114a2cf 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Queries/GetPaginatedEvents.cs +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Queries/GetPaginatedEvents.cs @@ -1,10 +1,11 @@ using Convey.CQRS.Queries; using MiniSpace.Services.Events.Application.DTO; +using MiniSpace.Services.Events.Core.Wrappers; using System.Collections.Generic; namespace MiniSpace.Services.Events.Application.Queries { - public class GetPaginatedEvents : IQuery> + public class GetPaginatedEvents : IQuery> { public int Page { get; set; } = 1; public int PageSize { get; set; } = 10; diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Queries/GetPaginatedOrganizerEvents.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Queries/GetPaginatedOrganizerEvents.cs index 6cf1e1bcf..c87907342 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Queries/GetPaginatedOrganizerEvents.cs +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Queries/GetPaginatedOrganizerEvents.cs @@ -1,10 +1,11 @@ using System; using Convey.CQRS.Queries; using MiniSpace.Services.Events.Application.DTO; +using MiniSpace.Services.Events.Core.Wrappers; namespace MiniSpace.Services.Events.Application.Queries { - public class GetPaginatedOrganizerEvents : IQuery> + public class GetPaginatedOrganizerEvents : IQuery> { public Guid OrganizerId { get; set; } public int Page { get; set; } = 1; diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Queries/GetPaginatedUserViews.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Queries/GetPaginatedUserViews.cs new file mode 100644 index 000000000..4acc765f2 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Queries/GetPaginatedUserViews.cs @@ -0,0 +1,14 @@ +using System; +using Convey.CQRS.Queries; +using MiniSpace.Services.Events.Application.DTO; +using MiniSpace.Services.Events.Core.Wrappers; + +namespace MiniSpace.Services.Events.Application.Queries +{ + public class GetPaginatedUserViews : IQuery> + { + public Guid UserId { get; set; } + public int Page { get; set; } = 1; + public int PageSize { get; set; } = 10; + } +} diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Queries/GetSearchEvents.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Queries/GetSearchEvents.cs index 64d2e61a6..50cbf6577 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Queries/GetSearchEvents.cs +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Queries/GetSearchEvents.cs @@ -2,10 +2,11 @@ using System.Collections.Generic; using Convey.CQRS.Queries; using MiniSpace.Services.Events.Application.DTO; +using MiniSpace.Services.Events.Core.Wrappers; namespace MiniSpace.Services.Events.Application.Queries { - public class GetSearchEvents : IQuery> + public class GetSearchEvents : IQuery> { public string Name { get; set; } public string Organizer { get; set; } diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Queries/GetUserEvents.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Queries/GetUserEvents.cs index 9d9dfe8cd..a24bdf88a 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Queries/GetUserEvents.cs +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Queries/GetUserEvents.cs @@ -3,12 +3,11 @@ using System.Diagnostics.CodeAnalysis; using Convey.CQRS.Queries; using MiniSpace.Services.Events.Application.DTO; -using MiniSpace.Services.Events.Application.Wrappers; - +using MiniSpace.Services.Events.Core.Wrappers; namespace MiniSpace.Services.Events.Application.Queries { [ExcludeFromCodeCoverage] - public class GetUserEvents : IQuery> + public class GetUserEvents : IQuery> { public Guid UserId { get; set; } public string EngagementType { get; set; } diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Queries/GetUserEventsFeed.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Queries/GetUserEventsFeed.cs new file mode 100644 index 000000000..16b8bd6da --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Queries/GetUserEventsFeed.cs @@ -0,0 +1,28 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using Convey.CQRS.Queries; +using MiniSpace.Services.Events.Application.DTO; +using MiniSpace.Services.Events.Core.Wrappers; + +namespace MiniSpace.Services.Events.Application.Queries +{ + [ExcludeFromCodeCoverage] + public class GetUserEventsFeed : IQuery> + { + public Guid UserId { get; set; } + public int PageNumber { get; set; } = 1; + public int PageSize { get; set; } = 10; + public string SortBy { get; set; } = "PublishDate"; + public string Direction { get; set; } = "asc"; + + public GetUserEventsFeed(Guid userId, int pageNumber = 1, int pageSize = 10, + string sortBy = "PublishDate", string direction = "asc") + { + UserId = userId; + PageNumber = pageNumber; + PageSize = pageSize; + SortBy = sortBy; + Direction = direction; + } + } +} diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Services/Clients/IFriendsServiceClient.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Services/Clients/IFriendsServiceClient.cs index 605483c2d..07357399d 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Services/Clients/IFriendsServiceClient.cs +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Services/Clients/IFriendsServiceClient.cs @@ -7,6 +7,6 @@ namespace MiniSpace.Services.Events.Application.Services.Clients { public interface IFriendsServiceClient { - Task> GetAsync(Guid studentId); + Task> GetAsync(Guid studentId); } } \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Services/Clients/IStudentsServiceClient.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Services/Clients/IStudentsServiceClient.cs index 1ed110d82..11885df3b 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Services/Clients/IStudentsServiceClient.cs +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Services/Clients/IStudentsServiceClient.cs @@ -8,5 +8,6 @@ public interface IStudentsServiceClient { Task GetAsync(Guid id); Task StudentExistsAsync(Guid id); + Task GetStudentByIdAsync(Guid studentId); } } \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Services/IEventRecommendationService.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Services/IEventRecommendationService.cs new file mode 100644 index 000000000..4a4d19586 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Services/IEventRecommendationService.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using MiniSpace.Services.Events.Application.DTO; + +namespace MiniSpace.Services.Events.Application.Services +{ + public interface IEventRecommendationService + { + IEnumerable RankEventsByUserInterest(Guid userId, IEnumerable events, IEnumerable userInterests); + } +} diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Services/IEventService.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Services/IEventService.cs index 7a3a9429c..486773f1e 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Services/IEventService.cs +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Services/IEventService.cs @@ -2,13 +2,13 @@ using System.Threading.Tasks; using MiniSpace.Services.Events.Application.Commands; using MiniSpace.Services.Events.Application.DTO; -using MiniSpace.Services.Events.Application.Wrappers; +using MiniSpace.Services.Events.Core.Wrappers; namespace MiniSpace.Services.Events.Application.Services { public interface IEventService { - Task>> BrowseEventsAsync(SearchEvents command); - Task>> BrowseOrganizerEventsAsync(SearchOrganizerEvents command); + Task> BrowseEventsAsync(SearchEvents command); + Task> BrowseOrganizerEventsAsync(SearchOrganizerEvents command); } } \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Entities/Comment.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Entities/Comment.cs new file mode 100644 index 000000000..380d20d50 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Entities/Comment.cs @@ -0,0 +1,59 @@ +using System; + +namespace MiniSpace.Services.Events.Core.Entities +{ + public class Comment + { + public Guid Id { get; private set; } + public Guid ContextId { get; private set; } + public string CommentContext { get; private set; } + public Guid UserId { get; private set; } + public Guid ParentId { get; private set; } + public string TextContent { get; private set; } + public DateTime CreatedAt { get; private set; } + public DateTime LastUpdatedAt { get; private set; } + public int RepliesCount { get; private set; } + public bool IsDeleted { get; private set; } + + public Comment(Guid id, Guid contextId, string commentContext, Guid userId, + Guid parentId, string textContent, DateTime createdAt, + DateTime lastUpdatedAt, int repliesCount, bool isDeleted) + { + Id = id; + ContextId = contextId; + CommentContext = commentContext; + UserId = userId; + ParentId = parentId; + TextContent = textContent; + CreatedAt = createdAt; + LastUpdatedAt = lastUpdatedAt; + RepliesCount = repliesCount; + IsDeleted = isDeleted; + } + + public void UpdateText(string newText, DateTime updatedAt) + { + TextContent = newText; + LastUpdatedAt = updatedAt; + } + + public void MarkAsDeleted() + { + IsDeleted = true; + TextContent = "[deleted]"; + } + + public void IncrementRepliesCount() + { + RepliesCount++; + } + + public void DecrementRepliesCount() + { + if (RepliesCount > 0) + { + RepliesCount--; + } + } + } +} diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Entities/EventSettings.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Entities/EventSettings.cs index a74438ae4..50994b963 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Entities/EventSettings.cs +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Entities/EventSettings.cs @@ -19,9 +19,9 @@ public class EventSettings public bool AllowComments { get; set; } public bool RequiresPayment { get; set; } - public PaymentMethod PaymentMethod { get; set; } // Specifies if payment is online or offline - public string PaymentReceiverDetails { get; set; } // Details of the payment receiver (e.g., account info) - public string PaymentGateway { get; set; } // Specifies the payment gateway (e.g., Stripe) + public PaymentMethod PaymentMethod { get; set; } + public string PaymentReceiverDetails { get; set; } + public string PaymentGateway { get; set; } public bool IssueTickets { get; set; } public int MaxTicketsPerPerson { get; set; } public decimal TicketPrice { get; set; } diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Entities/EventsViews.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Entities/EventsViews.cs new file mode 100644 index 000000000..cdb7eb9a7 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Entities/EventsViews.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; + +namespace MiniSpace.Services.Events.Core.Entities +{ + public class EventsViews + { + public Guid UserId { get; private set; } + public IEnumerable Views { get; private set; } + + public EventsViews(Guid userId, IEnumerable views) + { + UserId = userId; + Views = views ?? new List(); + } + + public void AddView(Guid eventId, DateTime date) + { + var viewList = new List(Views) + { + new View(eventId, date) + }; + Views = viewList; + } + + public void RemoveView(Guid eventId) + { + var viewList = new List(Views); + var viewToRemove = viewList.Find(view => view.EventId == eventId); + if (viewToRemove != null) + { + viewList.Remove(viewToRemove); + Views = viewList; + } + } + } +} diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Entities/Reaction.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Entities/Reaction.cs new file mode 100644 index 000000000..cd19619f5 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Entities/Reaction.cs @@ -0,0 +1,58 @@ +using System; + +namespace MiniSpace.Services.Events.Core.Entities +{ + public class Reaction + { + public Guid Id { get; private set; } + public Guid UserId { get; private set; } + public Guid ContentId { get; private set; } + public string Type { get; private set; } + public string ContentType { get; private set; } + public string TargetType { get; private set; } + public DateTime CreatedAt { get; private set; } + + private Reaction() { } + + private Reaction(Guid id, Guid userId, string type, Guid contentId, string contentType, string targetType) + { + if (string.IsNullOrWhiteSpace(type)) + { + throw new ArgumentException("Reaction type cannot be null or empty.", nameof(type)); + } + + if (string.IsNullOrWhiteSpace(contentType)) + { + throw new ArgumentException("Content type cannot be null or empty.", nameof(contentType)); + } + + if (string.IsNullOrWhiteSpace(targetType)) + { + throw new ArgumentException("Target type cannot be null or empty.", nameof(targetType)); + } + + Id = id != Guid.Empty ? id : throw new ArgumentException("Reaction ID cannot be empty.", nameof(id)); + UserId = userId != Guid.Empty ? userId : throw new ArgumentException("User ID cannot be empty.", nameof(userId)); + ContentId = contentId != Guid.Empty ? contentId : throw new ArgumentException("Content ID cannot be empty.", nameof(contentId)); + Type = type; + ContentType = contentType; + TargetType = targetType; + CreatedAt = DateTime.UtcNow; + } + + public static Reaction Create(Guid id, Guid userId, string type, Guid contentId, string contentType, string targetType) + { + return new Reaction(id, userId, type, contentId, contentType, targetType); + } + + public void UpdateReactionType(string newType) + { + if (string.IsNullOrWhiteSpace(newType)) + { + throw new ArgumentException("Reaction type cannot be null or empty.", nameof(newType)); + } + + Type = newType; + } + } +} diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Entities/View.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Entities/View.cs new file mode 100644 index 000000000..9372d3fd5 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Entities/View.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace MiniSpace.Services.Events.Core.Entities +{ + public class View + { + public Guid EventId { get; private set; } + public DateTime Date { get; private set; } + + public View(Guid eventId, DateTime date) + { + EventId = eventId; + Date = date; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Repositories/IEventsUserViewsRepository.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Repositories/IEventsUserViewsRepository.cs new file mode 100644 index 000000000..ccf4a7c9d --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Repositories/IEventsUserViewsRepository.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using MiniSpace.Services.Events.Core.Entities; + +namespace MiniSpace.Services.Events.Core.Repositories +{ + public interface IEventsUserViewsRepository + { + Task GetAsync(Guid userId); + Task AddAsync(EventsViews eventsViews); + Task UpdateAsync(EventsViews eventsViews); + Task DeleteAsync(Guid userId); + } +} diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Repositories/IUserCommentsHistoryRepository.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Repositories/IUserCommentsHistoryRepository.cs new file mode 100644 index 000000000..b6a50d66b --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Repositories/IUserCommentsHistoryRepository.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using MiniSpace.Services.Events.Core.Entities; +using MiniSpace.Services.Events.Core.Wrappers; + +namespace MiniSpace.Services.Events.Core.Repositories +{ + public interface IUserCommentsHistoryRepository + { + Task SaveCommentAsync(Guid userId, Comment comment); + + Task> GetUserCommentsAsync(Guid userId); + + Task> GetUserCommentsPagedAsync(Guid userId, int pageNumber, int pageSize); + + Task DeleteCommentAsync(Guid userId, Guid commentId); + } +} diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Repositories/IUserReactionsHistoryRepository.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Repositories/IUserReactionsHistoryRepository.cs new file mode 100644 index 000000000..945cb1e3d --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Repositories/IUserReactionsHistoryRepository.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using MiniSpace.Services.Events.Core.Entities; +using MiniSpace.Services.Events.Core.Wrappers; + +namespace MiniSpace.Services.Events.Core.Repositories +{ + public interface IUserReactionsHistoryRepository + { + Task SaveReactionAsync(Guid userId, Reaction reaction); + + Task> GetUserReactionsAsync(Guid userId); + + Task> GetUserReactionsPagedAsync(Guid userId, int pageNumber, int pageSize); + + Task DeleteReactionAsync(Guid userId, Guid reactionId); + } +} diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Wrappers/PagedResponse.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Wrappers/PagedResponse.cs index 4f5edc17f..017a5dbe6 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Wrappers/PagedResponse.cs +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Wrappers/PagedResponse.cs @@ -1,31 +1,35 @@ -using System.Diagnostics.CodeAnalysis; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; -namespace MiniSpace.Services.Events.Application.Wrappers +namespace MiniSpace.Services.Events.Core.Wrappers { [ExcludeFromCodeCoverage] - public class PagedResponse : Response + public class PagedResponse { + public IEnumerable Items { get; } public int TotalPages { get; } - public int TotalElements { get; } - public int Size { get; } - public int Number { get; } + public int TotalItems { get; } + public int PageSize { get; } + public int Page { get; } public bool First { get; } public bool Last { get; } public bool Empty { get; } + public int? NextPage => Page < TotalPages ? Page + 1 : (int?)null; + public int? PreviousPage => Page > 1 ? Page - 1 : (int?)null; - public PagedResponse(T content, int pageNumber, int pageSize, int totalPages, int totalElements) + public PagedResponse(IEnumerable items, int page, int pageSize, int totalItems) { - Content = content; - TotalPages = totalPages; - TotalElements = totalElements; - Size = pageSize; - Number = pageNumber; - First = pageNumber == 0; - Last = pageNumber == totalPages - 1; - Empty = totalElements == 0; - Succeeded = true; - Errors = null; - Message = null; + Items = items; + PageSize = pageSize; + TotalItems = totalItems; + TotalPages = pageSize > 0 ? (int)Math.Ceiling((decimal)totalItems / pageSize) : 0; + Page = page; + First = page == 1; + Last = page == TotalPages; + Empty = !items.Any(); } } } \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Extensions.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Extensions.cs index e84ef6592..0dd14dcdb 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Extensions.cs +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Extensions.cs @@ -49,6 +49,9 @@ using MiniSpace.Services.Events.Infrastructure.Services.Clients; using MiniSpace.Services.Events.Infrastructure.Services.Workers; using System.Diagnostics.CodeAnalysis; +using MiniSpace.Services.Posts.Infrastructure.Mongo.Repositories; +using MiniSpace.Services.Events.Infrastructure.Services.Recommendation; +using Microsoft.ML; namespace MiniSpace.Services.Events.Infrastructure { @@ -61,15 +64,22 @@ public static IConveyBuilder AddInfrastructure(this IConveyBuilder builder) builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); builder.Services.AddTransient(); + builder.Services.AddTransient(); builder.Services.AddTransient(); builder.Services.AddTransient(); builder.Services.AddTransient(); builder.Services.AddTransient(ctx => ctx.GetRequiredService().Create()); builder.Services.TryDecorate(typeof(ICommandHandler<>), typeof(OutboxCommandHandlerDecorator<>)); builder.Services.TryDecorate(typeof(IEventHandler<>), typeof(OutboxEventHandlerDecorator<>)); + builder.Services.AddSingleton(); builder.Services.AddHostedService(); return builder @@ -88,6 +98,9 @@ public static IConveyBuilder AddInfrastructure(this IConveyBuilder builder) .AddJaeger() .AddHandlersLogging() .AddMongoRepository("events") + .AddMongoRepository("events_views") + .AddMongoRepository("user_comments_history") + .AddMongoRepository("events") .AddWebApiSwaggerDocs() .AddSecurity(); } @@ -113,7 +126,9 @@ public static IApplicationBuilder UseInfrastructure(this IApplicationBuilder app .SubscribeCommand() .SubscribeCommand() .SubscribeEvent() - .SubscribeEvent(); + .SubscribeEvent() + .SubscribeEvent() + .SubscribeEvent(); return app; } diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/MiniSpace.Services.Events.Infrastructure.csproj b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/MiniSpace.Services.Events.Infrastructure.csproj index af01e2591..92b3b024b 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/MiniSpace.Services.Events.Infrastructure.csproj +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/MiniSpace.Services.Events.Infrastructure.csproj @@ -29,6 +29,10 @@ + + + + diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Documents/CommentDocument.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Documents/CommentDocument.cs new file mode 100644 index 000000000..1e8a23bdd --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Documents/CommentDocument.cs @@ -0,0 +1,19 @@ +using System; +using Convey.Types; + +namespace MiniSpace.Services.Events.Infrastructure.Mongo.Documents +{ + public class CommentDocument : IIdentifiable + { + public Guid Id { get; set; } + public Guid ContextId { get; set; } + public string CommentContext { get; set; } + public Guid UserId { get; set; } + public Guid ParentId { get; set; } + public string TextContent { get; set; } + public DateTime CreatedAt { get; set; } + public DateTime LastUpdatedAt { get; set; } + public int RepliesCount { get; set; } + public bool IsDeleted { get; set; } + } +} diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Documents/EventsViewsExtensions.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Documents/EventsViewsExtensions.cs new file mode 100644 index 000000000..a4d4052e8 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Documents/EventsViewsExtensions.cs @@ -0,0 +1,41 @@ +using System; +using System.Linq; +using MiniSpace.Services.Events.Application.DTO; +using MiniSpace.Services.Events.Core.Entities; + +namespace MiniSpace.Services.Events.Infrastructure.Mongo.Documents +{ + public static class EventsViewsExtensions + { + public static UserEventsViewsDto AsDto(this UserEventsViewsDocument document) + { + return new UserEventsViewsDto + { + UserId = document.UserId, + Views = document.Views.Select(v => new ViewDto + { + EventId = v.EventId, + Date = v.Date + }) + }; + } + + public static UserEventsViewsDocument AsDocument(this EventsViews entity) + { + return new UserEventsViewsDocument + { + Id = Guid.NewGuid(), + UserId = entity.UserId, + Views = entity.Views.Select(ViewDocument.FromEntity).ToList() + }; + } + + public static EventsViews AsEntity(this UserEventsViewsDocument document) + { + return new EventsViews( + document.UserId, + document.Views.Select(v => new View(v.EventId, v.Date)) + ); + } + } +} diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Documents/Extensions.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Documents/Extensions.cs index d2abe39f9..f1915a172 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Documents/Extensions.cs +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Documents/Extensions.cs @@ -46,11 +46,11 @@ public static EventDto AsDto(this Event @event, Guid studentId) Id = @event.Id, Name = @event.Name, Description = @event.Description, - Organizer = @event.Organizer.AsDto(), // Assuming an AsDto method exists for Organizer + Organizer = @event.Organizer.AsDto(), StartDate = @event.StartDate, EndDate = @event.EndDate, - Location = @event.Location.AsDto(), // Assuming an AsDto method exists for Address - MediaFilesUrl = @event.MediaFiles?.ToList(), // Converting MediaFiles to a list of URLs + Location = @event.Location.AsDto(), + MediaFilesUrl = @event.MediaFiles?.ToList(), BannerUrl = @event.BannerUrl, InterestedStudents = @event.InterestedParticipants.Count(), SignedUpStudents = @event.SignedUpParticipants.Count(), @@ -61,12 +61,12 @@ public static EventDto AsDto(this Event @event, Guid studentId) PublishDate = @event.PublishDate, UpdatedAt = @event.UpdatedAt, Visibility = @event.Visibility, - Settings = new EventSettingsDto(@event.Settings), // Assuming an AsDto method exists for EventSettings + Settings = new EventSettingsDto(@event.Settings), IsSignedUp = @event.SignedUpParticipants.Any(x => x.StudentId == studentId), IsInterested = @event.InterestedParticipants.Any(x => x.StudentId == studentId), StudentRating = @event.Ratings.FirstOrDefault(x => x.StudentId == studentId)?.Value, - FriendsInterestedIn = Enumerable.Empty(), // Placeholder, customize as needed - FriendsSignedUp = Enumerable.Empty() // Placeholder, customize as needed + FriendsInterestedIn = Enumerable.Empty(), + FriendsSignedUp = Enumerable.Empty() }; } @@ -240,5 +240,95 @@ public static RatingDto AsRatingDto(this RatingDocument document) public static Rating AsEntity(this RatingDocument document) => new (document.StudentId, document.Value); + + + public static CommentDocument AsDocument(this Comment comment) + { + return new CommentDocument + { + Id = comment.Id, + ContextId = comment.ContextId, + CommentContext = comment.CommentContext, + UserId = comment.UserId, + ParentId = comment.ParentId, + TextContent = comment.TextContent, + CreatedAt = comment.CreatedAt, + LastUpdatedAt = comment.LastUpdatedAt, + RepliesCount = comment.RepliesCount, + IsDeleted = comment.IsDeleted + }; + } + + public static Comment AsEntity(this CommentDocument document) + { + return new Comment( + document.Id, + document.ContextId, + document.CommentContext, + document.UserId, + document.ParentId, + document.TextContent, + document.CreatedAt, + document.LastUpdatedAt, + document.RepliesCount, + document.IsDeleted + ); + } + + public static UserCommentsDocument AsDocument(this IEnumerable comments, Guid userId) + { + return new UserCommentsDocument + { + Id = Guid.NewGuid(), + UserId = userId, + Comments = comments.Select(comment => comment.AsDocument()).ToList() + }; + } + + public static IEnumerable AsEntities(this UserCommentsDocument document) + { + return document.Comments.Select(doc => doc.AsEntity()); + } + + public static ReactionDocument AsDocument(this Reaction reaction) + { + return new ReactionDocument + { + Id = reaction.Id, + UserId = reaction.UserId, + ContentId = reaction.ContentId, + ContentType = reaction.ContentType, + ReactionType = reaction.Type, + TargetType = reaction.TargetType, + CreatedAt = reaction.CreatedAt + }; + } + + public static Reaction AsEntity(this ReactionDocument document) + { + return Reaction.Create( + document.Id, + document.UserId, + document.ReactionType, + document.ContentId, + document.ContentType, + document.TargetType + ); + } + + public static UserReactionDocument AsDocument(this IEnumerable reactions, Guid userId) + { + return new UserReactionDocument + { + Id = Guid.NewGuid(), + UserId = userId, + Reactions = reactions.Select(reaction => reaction.AsDocument()).ToList() + }; + } + + public static IEnumerable AsEntities(this UserReactionDocument document) + { + return document.Reactions.Select(doc => doc.AsEntity()); + } } } diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Documents/ReactionDocument.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Documents/ReactionDocument.cs new file mode 100644 index 000000000..321470768 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Documents/ReactionDocument.cs @@ -0,0 +1,15 @@ +using System; + +namespace MiniSpace.Services.Events.Infrastructure.Mongo.Documents +{ + public class ReactionDocument + { + public Guid Id { get; set; } + public Guid UserId { get; set; } + public Guid ContentId { get; set; } + public string ContentType { get; set; } + public string ReactionType { get; set; } + public string TargetType { get; set; } + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + } +} diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Documents/UserCommentsDocument.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Documents/UserCommentsDocument.cs new file mode 100644 index 000000000..793024de0 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Documents/UserCommentsDocument.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using Convey.Types; +using MongoDB.Bson.Serialization.Attributes; + +namespace MiniSpace.Services.Events.Infrastructure.Mongo.Documents +{ + public class UserCommentsDocument : IIdentifiable + { + [BsonId] + [BsonRepresentation(MongoDB.Bson.BsonType.String)] + public Guid Id { get; set; } + public Guid UserId { get; set; } + public IEnumerable Comments { get; set; } + } +} diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Documents/UserEventsViewsDocument.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Documents/UserEventsViewsDocument.cs new file mode 100644 index 000000000..cffe5a1ec --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Documents/UserEventsViewsDocument.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Convey.Types; +using MiniSpace.Services.Events.Core.Entities; + +namespace MiniSpace.Services.Events.Infrastructure.Mongo.Documents +{ + public class UserEventsViewsDocument : IIdentifiable + { + public Guid Id { get; set; } + public Guid UserId { get; set; } + public List Views { get; set; } = new List(); + + public static UserEventsViewsDocument FromEntity(EventsViews eventsViews) + { + return new UserEventsViewsDocument + { + Id = Guid.NewGuid(), + UserId = eventsViews.UserId, + Views = new List(eventsViews.Views.Select(ViewDocument.FromEntity)) + }; + } + + public EventsViews ToEntity() + { + return new EventsViews(UserId, Views.Select(view => view.ToEntity())); + } + } +} diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Documents/UserReactionDocument.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Documents/UserReactionDocument.cs new file mode 100644 index 000000000..6549c7b7a --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Documents/UserReactionDocument.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using Convey.Types; +using MongoDB.Bson.Serialization.Attributes; + +namespace MiniSpace.Services.Events.Infrastructure.Mongo.Documents +{ + public class UserReactionDocument : IIdentifiable + { + [BsonId] + [BsonRepresentation(MongoDB.Bson.BsonType.String)] + public Guid Id { get; set; } + public Guid UserId { get; set; } + public IEnumerable Reactions { get; set; } + } +} diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Documents/ViewDocument.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Documents/ViewDocument.cs new file mode 100644 index 000000000..74a3f40ee --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Documents/ViewDocument.cs @@ -0,0 +1,28 @@ +using System; +using Convey.Types; +using MiniSpace.Services.Events.Core.Entities; + +namespace MiniSpace.Services.Events.Infrastructure.Mongo.Documents +{ + public class ViewDocument : IIdentifiable + { + public Guid Id { get; set; } + public Guid EventId { get; set; } + public DateTime Date { get; set; } + + public static ViewDocument FromEntity(View view) + { + return new ViewDocument + { + Id = Guid.NewGuid(), + EventId = view.EventId, + Date = view.Date + }; + } + + public View ToEntity() + { + return new View(EventId, Date); + } + } +} diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Queries/Handlers/GetEventHandler.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Queries/Handlers/GetEventHandler.cs index 40f499e3e..678331a12 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Queries/Handlers/GetEventHandler.cs +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Queries/Handlers/GetEventHandler.cs @@ -36,24 +36,33 @@ public GetEventHandler(IMongoRepository eventRepository, public async Task HandleAsync(GetEvent query, CancellationToken cancellationToken) { var document = await _eventRepository.GetAsync(p => p.Id == query.EventId); - if(document is null) + if (document == null) { return null; } + var identity = _appContext.Identity; var friends = Enumerable.Empty(); - if(identity.IsAuthenticated) + + if (identity.IsAuthenticated) { - var result = await _friendsServiceClient.GetAsync(identity.Id); - if (result != null && result.Any()) + try + { + var userFriends = await _friendsServiceClient.GetAsync(identity.Id); + if (userFriends != null && userFriends.Any()) + { + friends = userFriends.SelectMany(uf => uf.Friends); + } + } + catch (Exception ex) { - friends = result.First().Friends; + Console.Error.WriteLine($"Error fetching friends: {ex.Message}"); + throw new ApplicationException("An error occurred while fetching friends data.", ex); } } await _messageBroker.PublishAsync(new EventViewed(query.EventId)); return document.AsDtoWithFriends(identity.Id, friends); } - } -} \ No newline at end of file +} diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Queries/Handlers/GetPaginatedEventsHandler.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Queries/Handlers/GetPaginatedEventsHandler.cs index 537194953..a56403972 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Queries/Handlers/GetPaginatedEventsHandler.cs +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Queries/Handlers/GetPaginatedEventsHandler.cs @@ -7,11 +7,12 @@ using MiniSpace.Services.Events.Application.DTO; using MiniSpace.Services.Events.Application.Queries; using MiniSpace.Services.Events.Core.Repositories; +using MiniSpace.Services.Events.Core.Wrappers; using MiniSpace.Services.Events.Infrastructure.Mongo.Documents; namespace MiniSpace.Services.Events.Infrastructure.Mongo.Queries.Handlers { - public class GetPaginatedEventsHandler : IQueryHandler> + public class GetPaginatedEventsHandler : IQueryHandler> { private readonly IEventRepository _eventRepository; private readonly IAppContext _appContext; @@ -22,30 +23,28 @@ public GetPaginatedEventsHandler(IEventRepository eventRepository, IAppContext a _appContext = appContext; } - public async Task> HandleAsync(GetPaginatedEvents query, CancellationToken cancellationToken) + public async Task> HandleAsync(GetPaginatedEvents query, CancellationToken cancellationToken) { - // Fetch the paginated events from the repository var (events, pageNumber, pageSize, totalPages, totalElements) = await _eventRepository.BrowseEventsAsync( pageNumber: query.Page, pageSize: query.PageSize, - name: string.Empty, // Assuming no filtering by name - organizer: string.Empty, // Assuming no filtering by organizer - dateFrom: default, // Assuming no date filter - dateTo: default, // Assuming no date filter - category: null, // Assuming no category filter - state: null, // Assuming no state filter - organizations: Enumerable.Empty(), // Assuming no organization filter - friends: Enumerable.Empty(), // Assuming no friends filter - friendsEngagementType: null, // Assuming no engagement type filter - sortBy: Enumerable.Empty(), // Assuming no sorting - direction: string.Empty // Assuming no sorting direction + name: string.Empty, + organizer: string.Empty, + dateFrom: default, + dateTo: default, + category: null, + state: null, + organizations: Enumerable.Empty(), + friends: Enumerable.Empty(), + friendsEngagementType: null, + sortBy: Enumerable.Empty(), + direction: string.Empty ); var studentId = _appContext.Identity.Id; var eventDtos = events.Select(e => e.AsDto(studentId)).ToList(); - // Return a paged result with the fetched data - return new MiniSpace.Services.Events.Application.DTO.PagedResult(eventDtos, pageNumber, pageSize, totalElements); + return new PagedResponse(eventDtos, pageNumber, pageSize, totalElements); } } } diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Queries/Handlers/GetPaginatedOrganizerEventsHandler.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Queries/Handlers/GetPaginatedOrganizerEventsHandler.cs index 1499d4630..242872cb4 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Queries/Handlers/GetPaginatedOrganizerEventsHandler.cs +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Queries/Handlers/GetPaginatedOrganizerEventsHandler.cs @@ -7,11 +7,12 @@ using MiniSpace.Services.Events.Application.DTO; using MiniSpace.Services.Events.Application.Queries; using MiniSpace.Services.Events.Core.Repositories; +using MiniSpace.Services.Events.Core.Wrappers; using MiniSpace.Services.Events.Infrastructure.Mongo.Documents; namespace MiniSpace.Services.Events.Infrastructure.Mongo.Queries.Handlers { - public class GetPaginatedOrganizerEventsHandler : IQueryHandler> + public class GetPaginatedOrganizerEventsHandler : IQueryHandler> { private readonly IEventRepository _eventRepository; private readonly IAppContext _appContext; @@ -22,7 +23,7 @@ public GetPaginatedOrganizerEventsHandler(IEventRepository eventRepository, IApp _appContext = appContext; } - public async Task> HandleAsync(GetPaginatedOrganizerEvents query, CancellationToken cancellationToken) + public async Task> HandleAsync(GetPaginatedOrganizerEvents query, CancellationToken cancellationToken) { var (events, pageNumber, pageSize, totalPages, totalElements) = await _eventRepository.BrowseOrganizerEventsAsync( pageNumber: query.Page, @@ -39,7 +40,7 @@ public GetPaginatedOrganizerEventsHandler(IEventRepository eventRepository, IApp var studentId = _appContext.Identity.Id; var eventDtos = events.Select(e => e.AsDto(studentId)).ToList(); - return new MiniSpace.Services.Events.Application.DTO.PagedResult(eventDtos, pageNumber, pageSize, totalElements); + return new PagedResponse(eventDtos, pageNumber, pageSize, totalElements); } } } diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Queries/Handlers/GetPaginatedSearchEventsHandler.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Queries/Handlers/GetPaginatedSearchEventsHandler.cs index 48c12954e..14bffc25e 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Queries/Handlers/GetPaginatedSearchEventsHandler.cs +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Queries/Handlers/GetPaginatedSearchEventsHandler.cs @@ -10,11 +10,12 @@ using MiniSpace.Services.Events.Application.Queries; using MiniSpace.Services.Events.Core.Entities; using MiniSpace.Services.Events.Core.Repositories; +using MiniSpace.Services.Events.Core.Wrappers; using MiniSpace.Services.Events.Infrastructure.Mongo.Documents; namespace MiniSpace.Services.Events.Infrastructure.Mongo.Queries.Handlers { - public class GetPaginatedSearchEventsHandler : IQueryHandler> + public class GetPaginatedSearchEventsHandler : IQueryHandler> { private readonly IEventRepository _eventRepository; private readonly IAppContext _appContext; @@ -25,14 +26,13 @@ public GetPaginatedSearchEventsHandler(IEventRepository eventRepository, IAppCon _appContext = appContext; } - public async Task> HandleAsync(GetSearchEvents query, CancellationToken cancellationToken) + public async Task> HandleAsync(GetSearchEvents query, CancellationToken cancellationToken) { - var jsonOptionsx = new JsonSerializerOptions { WriteIndented = true }; + var jsonOptionsx = new JsonSerializerOptions { WriteIndented = true }; var queryJson = JsonSerializer.Serialize(query, jsonOptionsx); Console.WriteLine("Query Object: "); Console.WriteLine(queryJson); - // Convert string values to corresponding enum types Category? category = null; State? state = null; EventEngagementType? engagementType = null; @@ -52,15 +52,12 @@ public GetPaginatedSearchEventsHandler(IEventRepository eventRepository, IAppCon engagementType = parsedEngagementType; } - // Use PageableDto for pagination and sorting var pageNumber = query.Pageable?.Page ?? 1; var pageSize = query.Pageable?.Size ?? 10; - // Handle sorting var sortBy = query.Pageable?.Sort?.SortBy ?? Enumerable.Empty(); var sortDirection = query.Pageable?.Sort?.Direction ?? string.Empty; - // Fetch the events based on the query parameters var (events, returnedPageNumber, returnedPageSize, totalPages, totalElements) = await _eventRepository.BrowseEventsAsync( pageNumber: pageNumber, pageSize: pageSize, @@ -77,19 +74,16 @@ public GetPaginatedSearchEventsHandler(IEventRepository eventRepository, IAppCon direction: sortDirection ); - // Map events to DTOs var studentId = _appContext.Identity.Id; var eventDtos = events.Select(e => e.AsDto(studentId)).ToList(); - var pagedResult = new MiniSpace.Services.Events.Application.DTO.PagedResult(eventDtos, pageNumber, pageSize, totalElements); + var pagedResult = new PagedResponse(eventDtos, pageNumber, pageSize, totalElements); - // Serialize the result to JSON and log it var jsonOptions = new JsonSerializerOptions { WriteIndented = true }; var jsonResult = JsonSerializer.Serialize(pagedResult, jsonOptions); Console.WriteLine("Search Results: "); Console.WriteLine(jsonResult); - // Return the paginated result return pagedResult; } } diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Queries/Handlers/GetPaginatedUserViewsHandler.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Queries/Handlers/GetPaginatedUserViewsHandler.cs new file mode 100644 index 000000000..06d9d7d90 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Queries/Handlers/GetPaginatedUserViewsHandler.cs @@ -0,0 +1,44 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Convey.CQRS.Queries; +using MiniSpace.Services.Events.Application.DTO; +using MiniSpace.Services.Events.Core.Repositories; +using MiniSpace.Services.Events.Core.Wrappers; + +namespace MiniSpace.Services.Events.Application.Queries.Handlers +{ + public class GetPaginatedUserViewsHandler : IQueryHandler> + { + private readonly IEventsUserViewsRepository _eventsUserViewsRepository; + + public GetPaginatedUserViewsHandler(IEventsUserViewsRepository eventsUserViewsRepository) + { + _eventsUserViewsRepository = eventsUserViewsRepository; + } + + public async Task> HandleAsync(GetPaginatedUserViews query, CancellationToken cancellationToken) + { + var userViews = await _eventsUserViewsRepository.GetAsync(query.UserId); + + if (userViews == null) + { + return new PagedResponse(Enumerable.Empty(), query.Page, query.PageSize, 0); + } + + var totalItems = userViews.Views.Count(); + var views = userViews.Views + .Skip((query.Page - 1) * query.PageSize) + .Take(query.PageSize) + .Select(view => new ViewDto + { + EventId = view.EventId, + Date = view.Date + }) + .ToList(); + + return new PagedResponse(views, query.Page, query.PageSize, totalItems); + } + } +} diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Queries/Handlers/GetUserEventsFeedHandler.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Queries/Handlers/GetUserEventsFeedHandler.cs new file mode 100644 index 000000000..f980a48cd --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Queries/Handlers/GetUserEventsFeedHandler.cs @@ -0,0 +1,122 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Convey.CQRS.Queries; +using MiniSpace.Services.Events.Application.DTO; +using MiniSpace.Services.Events.Application.Queries; +using MiniSpace.Services.Events.Application.Services; +using MiniSpace.Services.Events.Application.Services.Clients; +using MiniSpace.Services.Events.Core.Repositories; +using MiniSpace.Services.Events.Core.Wrappers; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using MiniSpace.Services.Events.Application; +using MiniSpace.Services.Events.Infrastructure.Mongo.Documents; + +namespace MiniSpace.Services.Events.Infrastructure.Queries.Handlers +{ + public class GetUserEventsFeedHandler : IQueryHandler> + { + private readonly IEventRepository _eventRepository; + private readonly IEventRecommendationService _eventRecommendationService; + private readonly IStudentsServiceClient _studentsServiceClient; + private readonly IAppContext _appContext; + private readonly ILogger _logger; + + public GetUserEventsFeedHandler( + IEventRepository eventRepository, + IEventRecommendationService eventRecommendationService, + IStudentsServiceClient studentsServiceClient, + IAppContext appContext, + ILogger logger) + { + _eventRepository = eventRepository; + _eventRecommendationService = eventRecommendationService; + _studentsServiceClient = studentsServiceClient; + _appContext = appContext; + _logger = logger; + } + + public async Task> HandleAsync(GetUserEventsFeed query, CancellationToken cancellationToken) + { + _logger.LogInformation("Handling GetUserEventsFeed query: {Query}", JsonConvert.SerializeObject(query)); + + // Fetch user data and events in parallel + var userTask = _studentsServiceClient.GetStudentByIdAsync(query.UserId); + var eventsTask = _eventRepository.BrowseEventsAsync( + query.PageNumber, + query.PageSize, + name: string.Empty, + organizer: string.Empty, + dateFrom: default, + dateTo: default, + category: null, + state: null, + organizations: Enumerable.Empty(), + friends: Enumerable.Empty(), + friendsEngagementType: null, + sortBy: new List { query.SortBy }, + direction: query.Direction + ); + + await Task.WhenAll(userTask, eventsTask); + + var user = userTask.Result; + var (events, _, _, _, _) = eventsTask.Result; + + if (user == null) + { + return new PagedResponse(Enumerable.Empty(), query.PageNumber, query.PageSize, 0); + } + + var studentId = _appContext.Identity.Id; + var eventDtos = events.Select(e => e.AsDto(studentId)).ToList(); + + // Rank events using the recommendation service + var rankedEvents = _eventRecommendationService.RankEventsByUserInterest(query.UserId, eventDtos, user.Interests); + + if (!rankedEvents.Any()) + { + // If no ranked events, fetch all events + _logger.LogInformation("No ranked events found, loading all events."); + var allEventsTask = _eventRepository.BrowseEventsAsync( + query.PageNumber, + query.PageSize, + name: string.Empty, + organizer: string.Empty, + dateFrom: default, + dateTo: default, + category: null, + state: null, + organizations: Enumerable.Empty(), + friends: Enumerable.Empty(), + friendsEngagementType: null, + sortBy: new List { query.SortBy }, + direction: query.Direction + ); + + await allEventsTask; + + var (allEvents, _, _, _, _) = allEventsTask.Result; + eventDtos = allEvents.Select(e => e.AsDto(studentId)).ToList(); + rankedEvents = eventDtos; // Use all events as the ranked events + } + + // Paginate the ranked events + var pagedEvents = rankedEvents + .Skip((query.PageNumber - 1) * query.PageSize) + .Take(query.PageSize) + .ToList(); + + var response = new PagedResponse(pagedEvents, query.PageNumber, query.PageSize, rankedEvents.Count()); + + // Log the response for debugging purposes + var jsonResponse = JsonConvert.SerializeObject(response, Formatting.Indented); + _logger.LogInformation("Response JSON: {JsonResponse}", jsonResponse); + + return response; + } + } +} diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Queries/Handlers/GetUserEventsHandler.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Queries/Handlers/GetUserEventsHandler.cs index 00e980776..b477b3c0c 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Queries/Handlers/GetUserEventsHandler.cs +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Queries/Handlers/GetUserEventsHandler.cs @@ -11,12 +11,13 @@ using MiniSpace.Services.Events.Application.Services; using MiniSpace.Services.Events.Application.Services.Clients; using MiniSpace.Services.Events.Core.Entities; +using MiniSpace.Services.Events.Core.Wrappers; using MiniSpace.Services.Events.Core.Repositories; namespace MiniSpace.Services.Events.Infrastructure.Mongo.Queries.Handlers { [ExcludeFromCodeCoverage] - public class GetUserEventsHandler : IQueryHandler> + public class GetUserEventsHandler : IQueryHandler> { private readonly IEventRepository _eventRepository; private readonly IStudentsServiceClient _studentsServiceClient; @@ -32,12 +33,12 @@ public GetUserEventsHandler(IEventRepository eventRepository, _appContext = appContext; } - public async Task> HandleAsync(GetUserEvents query, CancellationToken cancellationToken) + public async Task> HandleAsync(GetUserEvents query, CancellationToken cancellationToken) { var identity = _appContext.Identity; if (identity.IsAuthenticated && identity.Id != query.UserId) { - return new MiniSpace.Services.Events.Application.DTO.PagedResult(Enumerable.Empty(), 1, query.NumberOfResults, 0); + return new PagedResponse(Enumerable.Empty(), 1, query.NumberOfResults, 0); } int pageSize = query.NumberOfResults > 0 ? query.NumberOfResults : 10; @@ -55,7 +56,7 @@ public GetUserEventsHandler(IEventRepository eventRepository, var result = await _eventRepository.BrowseStudentEventsAsync(query.Page, pageSize, studentEventIds, Enumerable.Empty(), "asc"); - return new MiniSpace.Services.Events.Application.DTO.PagedResult( + return new PagedResponse( result.events.Select(e => new EventDto(e, identity.Id)), result.pageNumber, result.pageSize, diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Repositories/EventsUserViewsRepository.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Repositories/EventsUserViewsRepository.cs new file mode 100644 index 000000000..615606031 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Repositories/EventsUserViewsRepository.cs @@ -0,0 +1,43 @@ +using System; +using System.Threading.Tasks; +using MongoDB.Driver; +using MiniSpace.Services.Events.Core.Entities; +using MiniSpace.Services.Events.Core.Repositories; +using MiniSpace.Services.Events.Infrastructure.Mongo.Documents; +using Convey.Persistence.MongoDB; + +namespace MiniSpace.Services.Events.Infrastructure.Mongo.Repositories +{ + public class EventsUserViewsRepository : IEventsUserViewsRepository + { + private readonly IMongoRepository _repository; + + public EventsUserViewsRepository(IMongoRepository repository) + { + _repository = repository; + } + + public async Task GetAsync(Guid userId) + { + var document = await _repository.GetAsync(x => x.UserId == userId); + return document?.ToEntity(); + } + + public async Task AddAsync(EventsViews eventsViews) + { + var document = eventsViews.AsDocument(); + await _repository.AddAsync(document); + } + + public async Task UpdateAsync(EventsViews eventsViews) + { + var document = eventsViews.AsDocument(); + await _repository.UpdateAsync(document); + } + + public async Task DeleteAsync(Guid userId) + { + await _repository.DeleteAsync(x => x.UserId == userId); + } + } +} diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Repositories/UserCommentsHistoryMongoRepository.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Repositories/UserCommentsHistoryMongoRepository.cs new file mode 100644 index 000000000..beb750dc7 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Repositories/UserCommentsHistoryMongoRepository.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using MiniSpace.Services.Events.Core.Entities; +using MiniSpace.Services.Events.Core.Repositories; +using MiniSpace.Services.Events.Core.Wrappers; +using MiniSpace.Services.Events.Infrastructure.Mongo.Documents; +using MongoDB.Driver; + +namespace MiniSpace.Services.Events.Infrastructure.Mongo.Repositories +{ + public class UserCommentsHistoryMongoRepository : IUserCommentsHistoryRepository + { + private readonly IMongoCollection _collection; + + public UserCommentsHistoryMongoRepository(IMongoDatabase database) + { + _collection = database.GetCollection("user_reactions_history"); + } + + public async Task SaveCommentAsync(Guid userId, Comment comment) + { + var filter = Builders.Filter.Eq(uc => uc.UserId, userId); + + var commentDocument = comment.AsDocument(); + + var update = Builders.Update.Combine( + Builders.Update.Push(uc => uc.Comments, commentDocument), + Builders.Update.SetOnInsert(uc => uc.UserId, userId), + Builders.Update.SetOnInsert(uc => uc.Id, Guid.NewGuid()) + ); + + await _collection.UpdateOneAsync(filter, update, new UpdateOptions { IsUpsert = true }); + } + + public async Task> GetUserCommentsAsync(Guid userId) + { + var userCommentsDocument = await _collection.Find(uc => uc.UserId == userId).FirstOrDefaultAsync(); + return userCommentsDocument?.AsEntities() ?? Enumerable.Empty(); + } + + public async Task> GetUserCommentsPagedAsync(Guid userId, int pageNumber, int pageSize) + { + var userCommentsDocument = await _collection.Find(uc => uc.UserId == userId).FirstOrDefaultAsync(); + + if (userCommentsDocument == null) + { + return new PagedResponse(Enumerable.Empty(), pageNumber, pageSize, 0); + } + + var pagedComments = userCommentsDocument.Comments + .Skip((pageNumber - 1) * pageSize) + .Take(pageSize) + .Select(doc => doc.AsEntity()); + + return new PagedResponse(pagedComments, pageNumber, pageSize, userCommentsDocument.Comments.Count()); + } + + public async Task DeleteCommentAsync(Guid userId, Guid commentId) + { + var filter = Builders.Filter.And( + Builders.Filter.Eq(uc => uc.UserId, userId), + Builders.Filter.ElemMatch(uc => uc.Comments, c => c.Id == commentId) + ); + + var update = Builders.Update.PullFilter(uc => uc.Comments, c => c.Id == commentId); + await _collection.UpdateOneAsync(filter, update); + } + } +} diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Repositories/UserReactionsHistoryMongoRepository.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Repositories/UserReactionsHistoryMongoRepository.cs new file mode 100644 index 000000000..fb6e7e739 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Repositories/UserReactionsHistoryMongoRepository.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using MiniSpace.Services.Events.Core.Entities; +using MiniSpace.Services.Events.Core.Repositories; +using MiniSpace.Services.Events.Core.Wrappers; +using MiniSpace.Services.Events.Infrastructure.Mongo.Documents; +using MongoDB.Driver; + + +namespace MiniSpace.Services.Posts.Infrastructure.Mongo.Repositories +{ + public class UserReactionsHistoryMongoRepository : IUserReactionsHistoryRepository + { + private readonly IMongoCollection _collection; + + public UserReactionsHistoryMongoRepository(IMongoDatabase database) + { + _collection = database.GetCollection("user_reactions_history"); + } + + public async Task SaveReactionAsync(Guid userId, Reaction reaction) + { + var filter = Builders.Filter.Eq(ur => ur.UserId, userId); + + var reactionDocument = reaction.AsDocument(); + + var update = Builders.Update.Combine( + Builders.Update.Push(ur => ur.Reactions, reactionDocument), + Builders.Update.SetOnInsert(ur => ur.UserId, userId), + Builders.Update.SetOnInsert(ur => ur.Id, Guid.NewGuid()) + ); + + await _collection.UpdateOneAsync(filter, update, new UpdateOptions { IsUpsert = true }); + } + + public async Task> GetUserReactionsAsync(Guid userId) + { + var userReactionsDocument = await _collection.Find(ur => ur.UserId == userId).FirstOrDefaultAsync(); + return userReactionsDocument?.AsEntities() ?? Enumerable.Empty(); + } + + public async Task> GetUserReactionsPagedAsync(Guid userId, int pageNumber, int pageSize) + { + var userReactionsDocument = await _collection.Find(ur => ur.UserId == userId).FirstOrDefaultAsync(); + + if (userReactionsDocument == null) + { + return new PagedResponse(Enumerable.Empty(), pageNumber, pageSize, 0); + } + + var pagedReactions = userReactionsDocument.Reactions + .Skip((pageNumber - 1) * pageSize) + .Take(pageSize) + .Select(doc => doc.AsEntity()); + + return new PagedResponse(pagedReactions, pageNumber, pageSize, userReactionsDocument.Reactions.Count()); + } + + public async Task DeleteReactionAsync(Guid userId, Guid reactionId) + { + var filter = Builders.Filter.And( + Builders.Filter.Eq(ur => ur.UserId, userId), + Builders.Filter.ElemMatch(ur => ur.Reactions, r => r.Id == reactionId) + ); + + var update = Builders.Update.PullFilter(ur => ur.Reactions, r => r.Id == reactionId); + await _collection.UpdateOneAsync(filter, update); + } + } +} diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Services/Clients/FriendsServiceClient.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Services/Clients/FriendsServiceClient.cs index 601086ac5..89f5205ae 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Services/Clients/FriendsServiceClient.cs +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Services/Clients/FriendsServiceClient.cs @@ -4,6 +4,7 @@ using System.Threading.Tasks; using Convey.HTTP; using MiniSpace.Services.Events.Application.DTO; +using MiniSpace.Services.Events.Core.Wrappers; using MiniSpace.Services.Events.Application.Services.Clients; namespace MiniSpace.Services.Events.Infrastructure.Services.Clients @@ -20,7 +21,10 @@ public FriendsServiceClient(IHttpClient httpClient, HttpClientOptions options) _url = options.Services["friends"]; } - public Task> GetAsync(Guid studentId) - => _httpClient.GetAsync>($"{_url}/friends/{studentId}"); + public async Task> GetAsync(Guid studentId) + { + var pagedResponse = await _httpClient.GetAsync>($"{_url}/friends/{studentId}"); + return pagedResponse.Items; + } } } \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Services/Clients/StudentsServiceClient.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Services/Clients/StudentsServiceClient.cs index 59334c418..c8896736b 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Services/Clients/StudentsServiceClient.cs +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Services/Clients/StudentsServiceClient.cs @@ -28,5 +28,8 @@ public async Task StudentExistsAsync(Guid id) return response != null; } + public Task GetStudentByIdAsync(Guid studentId) + => _httpClient.GetAsync($"{_url}/students/{studentId}"); + } } \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Services/EventRecommendationService.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Services/EventRecommendationService.cs new file mode 100644 index 000000000..d3a6a6995 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Services/EventRecommendationService.cs @@ -0,0 +1,99 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.ML; +using Microsoft.ML.Data; +using MiniSpace.Services.Events.Application.DTO; +using MiniSpace.Services.Events.Application.Services; + +namespace MiniSpace.Services.Events.Infrastructure.Services.Recommendation +{ + public class EventRecommendationService : IEventRecommendationService + { + private readonly MLContext _mlContext; + private readonly ILogger _logger; + + public EventRecommendationService(ILogger logger) + { + _mlContext = new MLContext(); + _logger = logger; + } + + public IEnumerable RankEventsByUserInterest(Guid userId, IEnumerable events, IEnumerable userInterests) + { + _logger.LogInformation("Starting RankEventsByUserInterest method."); + var stopwatch = Stopwatch.StartNew(); + + // Convert user interests to HashSet for quick lookups + var userInterestsSet = new HashSet(userInterests, StringComparer.OrdinalIgnoreCase); + + // Prepare input data and filter out unlikely matches early to reduce the data size + var inputData = events + .Where(e => userInterestsSet.Any(interest => e.Description.Contains(interest, StringComparison.OrdinalIgnoreCase))) + .Select(e => CreateInputModel(e, userInterestsSet)) + .ToArray(); + + // Return early if no events match the user's interests + if (inputData.Length == 0) + { + _logger.LogInformation("No events matched user interests. Returning empty list."); + return Enumerable.Empty(); + } + + // Train model dynamically for the specific user + var userModel = TrainUserModel(inputData); + var predictionEngine = _mlContext.Model.CreatePredictionEngine(userModel); + + // Score events + var scoredEvents = inputData + .AsParallel() + .WithDegreeOfParallelism(Environment.ProcessorCount) + .Select(input => (Event: events.First(e => e.Id.ToString() == input.EventId), Score: predictionEngine.Predict(input).Score)) + .OrderByDescending(result => result.Score) + .Select(result => result.Event) + .ToList(); + + _logger.LogInformation("Completed RankEventsByUserInterest method in {TotalElapsedMilliseconds} ms.", stopwatch.ElapsedMilliseconds); + return scoredEvents; + } + + // Asynchronous version of the method + public async Task> RankEventsByUserInterestAsync(Guid userId, IEnumerable events, IEnumerable userInterests) + { + return await Task.Run(() => RankEventsByUserInterest(userId, events, userInterests)); + } + + private EventInputModel CreateInputModel(EventDto eventItem, HashSet userInterests) + { + var keywordMatches = userInterests.Count(interest => eventItem.Description.Contains(interest, StringComparison.OrdinalIgnoreCase)); + return new EventInputModel + { + EventId = eventItem.Id.ToString(), + TextLength = eventItem.Description?.Length ?? 0, + KeywordMatchCount = keywordMatches, + EventAgeDays = eventItem.StartDate != DateTime.MinValue ? (float)(DateTime.UtcNow - eventItem.StartDate).TotalDays : 0, + Label = keywordMatches // Use keyword matches as the label + }; + } + + private ITransformer TrainUserModel(EventInputModel[] inputData) + { + var trainingData = _mlContext.Data.LoadFromEnumerable(inputData); + + var dataProcessPipeline = _mlContext.Transforms.Concatenate("Features", nameof(EventInputModel.TextLength), + nameof(EventInputModel.KeywordMatchCount), + nameof(EventInputModel.EventAgeDays)); + + var trainer = _mlContext.Regression.Trainers.Sdca(labelColumnName: "Label", featureColumnName: "Features"); + var trainingPipeline = dataProcessPipeline.Append(trainer); + + // Train the model dynamically for the user + return trainingPipeline.Fit(trainingData); + } + } +} + diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Services/EventService.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Services/EventService.cs index 8c16deb4d..96a85cfab 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Services/EventService.cs +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Services/EventService.cs @@ -9,9 +9,9 @@ using MiniSpace.Services.Events.Application.Exceptions; using MiniSpace.Services.Events.Application.Services; using MiniSpace.Services.Events.Application.Services.Clients; -using MiniSpace.Services.Events.Application.Wrappers; using MiniSpace.Services.Events.Core.Entities; using MiniSpace.Services.Events.Core.Repositories; +using MiniSpace.Services.Events.Core.Wrappers; namespace MiniSpace.Services.Events.Infrastructure.Services { @@ -32,7 +32,7 @@ public EventService(IEventRepository eventRepository, IEventValidator eventValid _appContext = appContext; } - public async Task>> BrowseEventsAsync(SearchEvents command) + public async Task> BrowseEventsAsync(SearchEvents command) { var dateFrom = DateTime.MinValue; var dateTo = DateTime.MinValue; @@ -40,24 +40,25 @@ public async Task>> BrowseEventsAsync(Search State? state = null; EventEngagementType? friendsEngagementType = null; IEnumerable organizations = new List(); - if(command.DateFrom != string.Empty) + + if (command.DateFrom != string.Empty) { - dateFrom =_eventValidator.ParseDate(command.DateFrom, "DateFrom"); + dateFrom = _eventValidator.ParseDate(command.DateFrom, "DateFrom"); } - if(command.DateTo != string.Empty) + if (command.DateTo != string.Empty) { dateTo = _eventValidator.ParseDate(command.DateTo, "DateTo"); } - if(command.Category != string.Empty) + if (command.Category != string.Empty) { category = _eventValidator.ParseCategory(command.Category); } - if(command.State != string.Empty) + if (command.State != string.Empty) { state = _eventValidator.ParseState(command.State); state = _eventValidator.RestrictState(state); } - if(command.FriendsEngagementType != string.Empty) + if (command.FriendsEngagementType != string.Empty) { friendsEngagementType = _eventValidator.ParseEngagementType(command.FriendsEngagementType); } @@ -66,51 +67,63 @@ public async Task>> BrowseEventsAsync(Search organizations = await _organizationsServiceClient .GetAllChildrenOrganizations(command.OrganizationId) ?? new List(); } + (int pageNumber, int pageSize) = _eventValidator.PageFilter(command.Pageable.Page, command.Pageable.Size); - + var result = await _eventRepository.BrowseEventsAsync( pageNumber, pageSize, command.Name, command.Organizer, dateFrom, dateTo, category, state, organizations, command.Friends, friendsEngagementType, command.Pageable.Sort.SortBy, command.Pageable.Sort.Direction); - + var identity = _appContext.Identity; - var pagedEvents = new PagedResponse>(result.events.Select(e => new EventDto(e, identity.Id)), - result.pageNumber, result.pageSize, result.totalPages, result.totalElements); + var pagedEvents = new PagedResponse( + result.events.Select(e => new EventDto(e, identity.Id)), + result.pageNumber, + result.pageSize, + result.totalElements + ); return pagedEvents; } - - public async Task>> BrowseOrganizerEventsAsync(SearchOrganizerEvents command) + + public async Task> BrowseOrganizerEventsAsync(SearchOrganizerEvents command) { var identity = _appContext.Identity; - if(identity.IsAuthenticated && identity.Id != command.OrganizerId && !identity.IsAdmin) + if (identity.IsAuthenticated && identity.Id != command.OrganizerId && !identity.IsAdmin) { throw new UnauthorizedOrganizerEventsAccessException(command.OrganizerId, identity.Id); } + var dateFrom = DateTime.MinValue; var dateTo = DateTime.MinValue; State? state = null; - if(command.DateFrom != string.Empty) + + if (command.DateFrom != string.Empty) { - dateFrom =_eventValidator.ParseDate(command.DateFrom, "DateFrom"); + dateFrom = _eventValidator.ParseDate(command.DateFrom, "DateFrom"); } - if(command.DateTo != string.Empty) + if (command.DateTo != string.Empty) { dateTo = _eventValidator.ParseDate(command.DateTo, "DateTo"); } - if(command.State != string.Empty) + if (command.State != string.Empty) { state = _eventValidator.ParseState(command.State); } + (int pageNumber, int pageSize) = _eventValidator.PageFilter(command.Pageable.Page, command.Pageable.Size); - + var result = await _eventRepository.BrowseOrganizerEventsAsync( - pageNumber, pageSize, command.Name, command.OrganizerId, dateFrom, dateTo, + pageNumber, pageSize, command.Name, command.OrganizerId, dateFrom, dateTo, command.Pageable.Sort.SortBy, command.Pageable.Sort.Direction, state); - - var pagedEvents = new PagedResponse>(result.events.Select(e => new EventDto(e, _appContext.Identity.Id)), - result.pageNumber, result.pageSize, result.totalPages, result.totalElements); + + var pagedEvents = new PagedResponse( + result.events.Select(e => new EventDto(e, _appContext.Identity.Id)), + result.pageNumber, + result.pageSize, + result.totalElements + ); return pagedEvents; } } -} \ No newline at end of file +} diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Services/Recommendation/EventInputModel.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Services/Recommendation/EventInputModel.cs new file mode 100644 index 000000000..bd0713a6b --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Services/Recommendation/EventInputModel.cs @@ -0,0 +1,24 @@ +using System; +using Microsoft.ML.Data; + +namespace MiniSpace.Services.Events.Infrastructure.Services.Recommendation +{ + public class EventInputModel + { + [LoadColumn(0)] + public string EventId { get; set; } // Convert Guid to string to ensure compatibility + + [LoadColumn(1)] + public float TextLength { get; set; } + + [LoadColumn(2)] + public float KeywordMatchCount { get; set; } + + [LoadColumn(3)] + public float EventAgeDays { get; set; } + + [LoadColumn(4)] + public float Label { get; set; } + } + +} diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Services/Recommendation/EventPrediction.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Services/Recommendation/EventPrediction.cs new file mode 100644 index 000000000..c8219c5cf --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Services/Recommendation/EventPrediction.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.ML.Data; + +namespace MiniSpace.Services.Events.Infrastructure.Services.Recommendation +{ + public class EventPrediction + { + [ColumnName("Score")] + public float Score { get; set; } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Api/Program.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Api/Program.cs index 50209eaab..d700f1dfa 100644 --- a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Api/Program.cs +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Api/Program.cs @@ -45,7 +45,8 @@ public static async Task Main(string[] args) .Get>("friends/pending/all") .Get>("friends/requests/sent/{userId}") - + .Get>("friends/{userId}/followers") + .Get>("friends/{userId}/following") .Put("friends/requests/{userId}/withdraw", afterDispatch: (cmd, ctx) => ctx.Response.Ok()) diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Queries/GetFollowers.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Queries/GetFollowers.cs new file mode 100644 index 000000000..3a6b8c730 --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Queries/GetFollowers.cs @@ -0,0 +1,20 @@ +using Convey.CQRS.Queries; +using MiniSpace.Services.Friends.Application.Dto; +using MiniSpace.Services.Friends.Core.Wrappers; + +namespace MiniSpace.Services.Friends.Application.Queries +{ + public class GetFollowers : IQuery> + { + public Guid UserId { get; set; } + public int Page { get; set; } + public int PageSize { get; set; } + + public GetFollowers(Guid userId, int page = 1, int pageSize = 10) + { + UserId = userId; + Page = page; + PageSize = pageSize; + } + } +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Queries/GetFollowing.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Queries/GetFollowing.cs new file mode 100644 index 000000000..4a052a45d --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Queries/GetFollowing.cs @@ -0,0 +1,20 @@ +using Convey.CQRS.Queries; +using MiniSpace.Services.Friends.Application.Dto; +using MiniSpace.Services.Friends.Core.Wrappers; + +namespace MiniSpace.Services.Friends.Application.Queries +{ + public class GetFollowing : IQuery> + { + public Guid UserId { get; set; } + public int Page { get; set; } + public int PageSize { get; set; } + + public GetFollowing(Guid userId, int page = 1, int pageSize = 10) + { + UserId = userId; + Page = page; + PageSize = pageSize; + } + } +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Queries/Handlers/GetFollowersHandler.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Queries/Handlers/GetFollowersHandler.cs new file mode 100644 index 000000000..f156041d5 --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Queries/Handlers/GetFollowersHandler.cs @@ -0,0 +1,80 @@ +using Convey.CQRS.Queries; +using Convey.Persistence.MongoDB; +using MiniSpace.Services.Friends.Application.Dto; +using MiniSpace.Services.Friends.Application.Queries; +using MiniSpace.Services.Friends.Core.Repositories; +using MiniSpace.Services.Friends.Core.Wrappers; +using MiniSpace.Services.Friends.Infrastructure.Mongo.Documents; +using MongoDB.Driver; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace MiniSpace.Services.Friends.Infrastructure.Mongo.Queries.Handlers +{ + public class GetFollowersHandler : IQueryHandler> + { + private readonly IUserFriendsRepository _userFriendsRepository; + private readonly IMongoRepository _userRequestsRepository; + + public GetFollowersHandler(IUserFriendsRepository userFriendsRepository, IMongoRepository userRequestsRepository) + { + _userFriendsRepository = userFriendsRepository; + _userRequestsRepository = userRequestsRepository; + } + + public async Task> HandleAsync(GetFollowers query, CancellationToken cancellationToken) + { + // Step 1: Get all users who have sent friend requests to the user (followers) + var incomingRequestsFilter = Builders.Filter.Eq(doc => doc.UserId, query.UserId); + var incomingRequests = await _userRequestsRepository.Collection + .Find(incomingRequestsFilter) + .ToListAsync(cancellationToken); + + var followersFromRequests = incomingRequests + .SelectMany(doc => doc.FriendRequests + .Where(request => request.InviteeId == query.UserId && request.State == Core.Entities.FriendState.Requested) + .Select(request => new FriendDto + { + Id = request.Id, + UserId = request.InviterId, + FriendId = request.InviteeId, + CreatedAt = request.RequestedAt, + State = request.State + })) + .ToList(); + + // Step 2: Get all friends of the user (friends are also followers) + var friends = await _userFriendsRepository.GetFriendsAsync(query.UserId); + + var followersFromFriends = friends.Select(f => new FriendDto + { + Id = f.Id, + UserId = f.FriendId, + FriendId = f.UserId, + CreatedAt = f.CreatedAt, + State = f.FriendState + }).ToList(); + + // Step 3: Combine followers from requests and friends + var allFollowers = followersFromRequests.Concat(followersFromFriends).Distinct().ToList(); + + // Step 4: Paginate the combined followers list + var totalItems = allFollowers.Count; + var paginatedFollowers = allFollowers + .Skip((query.Page - 1) * query.PageSize) + .Take(query.PageSize) + .ToList(); + + var response = new UserFriendsDto + { + UserId = query.UserId, + Friends = paginatedFollowers + }; + + return new PagedResponse(new List { response }, query.Page, query.PageSize, totalItems); + } + } +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Queries/Handlers/GetFollowingHandler.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Queries/Handlers/GetFollowingHandler.cs new file mode 100644 index 000000000..cebd0533f --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Queries/Handlers/GetFollowingHandler.cs @@ -0,0 +1,74 @@ +using Convey.CQRS.Queries; +using Convey.Persistence.MongoDB; +using MiniSpace.Services.Friends.Application.Dto; +using MiniSpace.Services.Friends.Application.Queries; +using MiniSpace.Services.Friends.Core.Repositories; +using MiniSpace.Services.Friends.Core.Wrappers; +using MiniSpace.Services.Friends.Infrastructure.Mongo.Documents; +using MongoDB.Driver; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace MiniSpace.Services.Friends.Infrastructure.Mongo.Queries.Handlers +{ + public class GetFollowingHandler : IQueryHandler> + { + private readonly IUserFriendsRepository _userFriendsRepository; + private readonly IMongoRepository _userRequestsRepository; + + public GetFollowingHandler(IUserFriendsRepository userFriendsRepository, IMongoRepository userRequestsRepository) + { + _userFriendsRepository = userFriendsRepository; + _userRequestsRepository = userRequestsRepository; + } + + public async Task> HandleAsync(GetFollowing query, CancellationToken cancellationToken) + { + var friends = await _userFriendsRepository.GetFriendsAsync(query.UserId); + + var sentRequestsFilter = Builders.Filter.Eq(doc => doc.UserId, query.UserId); + var sentRequests = await _userRequestsRepository.Collection + .Find(sentRequestsFilter) + .ToListAsync(cancellationToken); + + var following = friends.Select(f => new FriendDto + { + Id = f.Id, + UserId = f.UserId, + FriendId = f.FriendId, + CreatedAt = f.CreatedAt, + State = f.FriendState + }).ToList(); + + var sentRequestDtos = sentRequests.SelectMany(doc => doc.FriendRequests + .Where(request => request.InviterId == query.UserId && request.State == Core.Entities.FriendState.Requested) + .Select(request => new FriendDto + { + Id = request.Id, + UserId = request.InviterId, + FriendId = request.InviteeId, // The invitee is the one being followed + CreatedAt = request.RequestedAt, + State = request.State + })).ToList(); + + following.AddRange(sentRequestDtos); + + var totalItems = following.Count; + var paginatedFollowing = following + .Skip((query.Page - 1) * query.PageSize) + .Take(query.PageSize) + .ToList(); + + var response = new UserFriendsDto + { + UserId = query.UserId, + Friends = paginatedFollowing + }; + + return new PagedResponse(new List { response }, query.Page, query.PageSize, totalItems); + } + } +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Queries/Handlers/GetFriendsHandler.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Queries/Handlers/GetFriendsHandler.cs index 432282976..b6c94b422 100644 --- a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Queries/Handlers/GetFriendsHandler.cs +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Queries/Handlers/GetFriendsHandler.cs @@ -3,9 +3,11 @@ using MiniSpace.Services.Friends.Application.Queries; using MiniSpace.Services.Friends.Core.Repositories; using MiniSpace.Services.Friends.Core.Wrappers; +using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; using System.Linq; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; @@ -14,18 +16,23 @@ namespace MiniSpace.Services.Friends.Infrastructure.Mongo.Queries.Handlers public class GetFriendsHandler : IQueryHandler> { private readonly IUserFriendsRepository _userFriendsRepository; + private readonly ILogger _logger; - public GetFriendsHandler(IUserFriendsRepository userFriendsRepository) + public GetFriendsHandler(IUserFriendsRepository userFriendsRepository, ILogger logger) { _userFriendsRepository = userFriendsRepository; + _logger = logger; } public async Task> HandleAsync(GetFriends query, CancellationToken cancellationToken) { + _logger.LogInformation("Handling GetFriends query for UserId: {UserId}", query.UserId); + var allFriends = await _userFriendsRepository.GetFriendsAsync(query.UserId); if (allFriends == null || !allFriends.Any()) { + _logger.LogWarning("No friends found for UserId: {UserId}", query.UserId); return new PagedResponse(Enumerable.Empty(), query.Page, query.PageSize, 0); } @@ -52,6 +59,10 @@ public async Task> HandleAsync(GetFriends query, C var userFriendsDtos = new List { userFriendsDto }; + var jsonOptions = new JsonSerializerOptions { WriteIndented = true }; + var jsonString = JsonSerializer.Serialize(userFriendsDtos, jsonOptions); + _logger.LogInformation("Serialized UserFriendsDto JSON: {JsonString}", jsonString); + return new PagedResponse(userFriendsDtos, query.Page, query.PageSize, totalItems); } } diff --git a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Api/Program.cs b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Api/Program.cs index 04323e506..4cc556507 100644 --- a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Api/Program.cs +++ b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Api/Program.cs @@ -114,6 +114,11 @@ public static async Task Main(string[] args) var token = await ctx.RequestServices.GetService().VerifyTwoFactorCodeAsync(cmd); await ctx.Response.WriteJsonAsync(token); }) + .Put("users/status", async (cmd, ctx) => + { + await ctx.RequestServices.GetService().UpdateUserStatusAsync(cmd); + ctx.Response.StatusCode = 204; + }) )) .UseLogging() .Build() diff --git a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/Commands/SignIn.cs b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/Commands/SignIn.cs index 5c4dbd598..1960c3c4e 100644 --- a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/Commands/SignIn.cs +++ b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/Commands/SignIn.cs @@ -7,11 +7,13 @@ public class SignIn : ICommand { public string Email { get; set; } public string Password { get; set; } + public string DeviceType { get; set; } - public SignIn(string email, string password) + public SignIn(string email, string password, string deviceType) { Email = email; Password = password; + DeviceType = deviceType; } } -} \ No newline at end of file +} diff --git a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/Commands/UpdateUserStatus.cs b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/Commands/UpdateUserStatus.cs new file mode 100644 index 000000000..a935d7305 --- /dev/null +++ b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/Commands/UpdateUserStatus.cs @@ -0,0 +1,19 @@ +using System; +using Convey.CQRS.Commands; + +namespace MiniSpace.Services.Identity.Application.Commands +{ + public class UpdateUserStatus : ICommand + { + public Guid UserId { get; set; } + public bool IsOnline { get; set; } + public string DeviceType { get; set; } + + public UpdateUserStatus(Guid userId, bool isOnline, string deviceType) + { + UserId = userId; + IsOnline = isOnline; + DeviceType = deviceType; + } + } +} diff --git a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/DTO/AuthDto.cs b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/DTO/AuthDto.cs index 736294d90..fad14dd4c 100644 --- a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/DTO/AuthDto.cs +++ b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/DTO/AuthDto.cs @@ -11,6 +11,8 @@ public class AuthDto public string Role { get; set; } public long Expires { get; set; } public bool IsTwoFactorRequired { get; set; } - public Guid UserId { get; set; } + public Guid UserId { get; set; } + public bool IsOnline { get; set; } + public string DeviceType { get; set; } } } diff --git a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/DTO/UserDto.cs b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/DTO/UserDto.cs index d687cabf5..4708ac6fc 100644 --- a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/DTO/UserDto.cs +++ b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/DTO/UserDto.cs @@ -19,9 +19,11 @@ public class UserDto public bool IsTwoFactorEnabled { get; set; } public string TwoFactorSecret { get; set; } - public UserDto() - { - } + public bool IsOnline { get; set; } + public string DeviceType { get; set; } + public DateTime? LastActive { get; set; } + + public UserDto() { } public UserDto(User user) { @@ -35,6 +37,10 @@ public UserDto(User user) EmailVerifiedAt = user.EmailVerifiedAt; IsTwoFactorEnabled = user.IsTwoFactorEnabled; TwoFactorSecret = user.TwoFactorSecret; + + IsOnline = user.IsOnline; + DeviceType = user.DeviceType; + LastActive = user.LastActive; } } } diff --git a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/IIdentityContext.cs b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/IIdentityContext.cs index d5f13474d..16e10067b 100644 --- a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/IIdentityContext.cs +++ b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/IIdentityContext.cs @@ -12,7 +12,6 @@ public interface IIdentityContext bool IsAuthenticated { get; } bool IsAdmin { get; } bool IsBanned { get; } - bool IsOrganizer { get; } IDictionary Claims { get; } } } \ No newline at end of file diff --git a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/Services/IIdentityService.cs b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/Services/IIdentityService.cs index f92eebbf9..83b2d93a8 100644 --- a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/Services/IIdentityService.cs +++ b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/Services/IIdentityService.cs @@ -20,5 +20,6 @@ public interface IIdentityService Task DisableTwoFactorAsync(DisableTwoFactor command); Task GenerateTwoFactorSecretAsync(GenerateTwoFactorSecret command); Task VerifyTwoFactorCodeAsync(VerifyTwoFactorCode command); + Task UpdateUserStatusAsync(UpdateUserStatus command); } } \ No newline at end of file diff --git a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/Services/Identity/IdentityService.cs b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/Services/Identity/IdentityService.cs index 3a2b16074..2c68cb59f 100644 --- a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/Services/Identity/IdentityService.cs +++ b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/Services/Identity/IdentityService.cs @@ -94,16 +94,21 @@ public async Task SignInAsync(SignIn command) { claims.Add("permissions", user.Permissions); } + + user.SetOnlineStatus(true, command.DeviceType); + await _userRepository.UpdateAsync(user); + var auth = _jwtProvider.Create(user.Id, user.Role, claims: claims); auth.RefreshToken = await _refreshTokenService.CreateAsync(user.Id); + auth.IsOnline = true; + auth.DeviceType = command.DeviceType; + await _messageBroker.PublishAsync(new SignedIn(user.Id, user.Role)); return auth; } - - public async Task SignUpAsync(SignUp command) { if (!EmailRegex.IsMatch(command.Email)) @@ -318,5 +323,20 @@ public async Task VerifyTwoFactorCodeAsync(VerifyTwoFactorCode command) await _messageBroker.PublishAsync(new SignedIn(user.Id, user.Role)); return auth; } + + public async Task UpdateUserStatusAsync(UpdateUserStatus command) + { + var user = await _userRepository.GetAsync(command.UserId); + if (user == null) + { + throw new UserNotFoundException(command.UserId); + } + + user.SetOnlineStatus(command.IsOnline, command.DeviceType); + await _userRepository.UpdateAsync(user); + + _logger.LogInformation($"Updated status for user {command.UserId}: Online = {command.IsOnline}, DeviceType = {command.DeviceType}"); + } + } } diff --git a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/Services/Identity/RefreshTokenService.cs b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/Services/Identity/RefreshTokenService.cs index 76a231e56..0af9993a6 100644 --- a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/Services/Identity/RefreshTokenService.cs +++ b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/Services/Identity/RefreshTokenService.cs @@ -45,22 +45,37 @@ public async Task RevokeAsync(string refreshToken) token.Revoke(DateTime.UtcNow); await _refreshTokenRepository.UpdateAsync(token); + + var user = await _userRepository.GetAsync(token.UserId); + if (user != null) + { + user.SetOnlineStatus(false, null); + await _userRepository.UpdateAsync(user); + } } public async Task UseAsync(string refreshToken) { var token = await _refreshTokenRepository.GetAsync(refreshToken); - if (token is null) - { - throw new InvalidRefreshTokenException(); - } + User user = null; - if (token.Revoked) + if (token is null || token.Revoked) { - throw new RevokedRefreshTokenException(); + if (token?.UserId != null) + { + user = await _userRepository.GetAsync(token.UserId); + } + + if (user != null) + { + user.SetOnlineStatus(false, null); + await _userRepository.UpdateAsync(user); + } + + throw new InvalidRefreshTokenException(); } - var user = await _userRepository.GetAsync(token.UserId); + user = await _userRepository.GetAsync(token.UserId); if (user is null) { throw new UserNotFoundException(token.UserId); @@ -72,6 +87,7 @@ public async Task UseAsync(string refreshToken) ["permissions"] = user.Permissions } : null; + var auth = _jwtProvider.Create(token.UserId, user.Role, claims: claims); auth.RefreshToken = refreshToken; diff --git a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Core/Entities/User.cs b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Core/Entities/User.cs index 09e8d1fb1..fc28cf090 100644 --- a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Core/Entities/User.cs +++ b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Core/Entities/User.cs @@ -15,9 +15,12 @@ public class User : AggregateRoot public IEnumerable Permissions { get; private set; } public bool IsEmailVerified { get; set; } public string EmailVerificationToken { get; set; } - public DateTime? EmailVerifiedAt { get; set; } + public DateTime? EmailVerifiedAt { get; set; } public bool IsTwoFactorEnabled { get; set; } public string TwoFactorSecret { get; set; } + public bool IsOnline { get; private set; } + public string DeviceType { get; private set; } + public DateTime? LastActive { get; private set; } public User(Guid id, string name, string email, string password, Role role, DateTime createdAt, IEnumerable permissions = null) @@ -26,7 +29,7 @@ public User(Guid id, string name, string email, string password, Role role, Date { throw new InvalidNameException(name); } - + if (string.IsNullOrWhiteSpace(email)) { throw new InvalidEmailException(email); @@ -49,10 +52,14 @@ public User(Guid id, string name, string email, string password, Role role, Date Role = role; CreatedAt = createdAt; Permissions = permissions ?? Enumerable.Empty(); + + IsOnline = false; + DeviceType = null; + LastActive = DateTime.UtcNow; } internal User(Guid id, string name, string email, string password, Role role, DateTime createdAt, - bool isEmailVerified, string emailVerificationToken, DateTime? emailVerifiedAt, + bool isEmailVerified, string emailVerificationToken, DateTime? emailVerifiedAt, bool isTwoFactorEnabled, string twoFactorSecret, IEnumerable permissions = null) : this(id, name, email, password, role, createdAt, permissions) { @@ -62,7 +69,19 @@ internal User(Guid id, string name, string email, string password, Role role, Da IsTwoFactorEnabled = isTwoFactorEnabled; TwoFactorSecret = twoFactorSecret; } - + + public void SetOnlineStatus(bool isOnline, string deviceType) + { + IsOnline = isOnline; + DeviceType = isOnline ? deviceType : null; + LastActive = DateTime.UtcNow; + } + + public void UpdateLastActive() + { + LastActive = DateTime.UtcNow; + } + public void Ban() { if (Role == Role.Banned || Role == Role.Admin) @@ -72,7 +91,7 @@ public void Ban() Role = Role.Banned; } - + public void Unban() { if (Role != Role.Banned) @@ -137,6 +156,4 @@ public static class UserPermissions { public static string OrganizeEvents { get; private set; } = "organize_events"; } - - } diff --git a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Core/Exceptions/UserCannotBeBannedException.cs b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Core/Exceptions/UserCannotBeBannedException.cs index 1aabc2fb9..7591334ac 100644 --- a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Core/Exceptions/UserCannotBeBannedException.cs +++ b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Core/Exceptions/UserCannotBeBannedException.cs @@ -8,7 +8,8 @@ public class UserCannotBeBannedException : DomainException public Guid UserId { get; } public string Role { get; } - public UserCannotBeBannedException(Guid userId, string role) : base($"User with ID: {userId} and Role: {role} cannot be banned.") + public UserCannotBeBannedException(Guid userId, string role) : + base($"User with ID: {userId} and Role: {role} cannot be banned.") { UserId = userId; Role = role; diff --git a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Core/Exceptions/UserIsNotBannedException.cs b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Core/Exceptions/UserIsNotBannedException.cs index 34cfe793c..18491ded3 100644 --- a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Core/Exceptions/UserIsNotBannedException.cs +++ b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Core/Exceptions/UserIsNotBannedException.cs @@ -8,7 +8,8 @@ public class UserIsNotBannedException : DomainException public Guid UserId { get; } public string Role { get; } - public UserIsNotBannedException(Guid userId, string role) : base($"User with ID: {userId} and Role: {role} is not banned.") + public UserIsNotBannedException(Guid userId, string role) : + base($"User with ID: {userId} and Role: {role} is not banned.") { UserId = userId; Role = role; diff --git a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Infrastructure/Contexts/IdentityContext.cs b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Infrastructure/Contexts/IdentityContext.cs index e5b98897a..2738fe72f 100644 --- a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Infrastructure/Contexts/IdentityContext.cs +++ b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Infrastructure/Contexts/IdentityContext.cs @@ -15,7 +15,6 @@ internal class IdentityContext : IIdentityContext public bool IsAuthenticated { get; } public bool IsAdmin { get; } public bool IsBanned { get; } - public bool IsOrganizer { get; } public IDictionary Claims { get; } = new Dictionary(); internal IdentityContext() @@ -34,7 +33,6 @@ internal IdentityContext(string id, string role, bool isAuthenticated, IDictiona IsAuthenticated = isAuthenticated; IsAdmin = Role.Equals("admin", StringComparison.InvariantCultureIgnoreCase); IsBanned = Role.Equals("banned", StringComparison.InvariantCultureIgnoreCase); - IsOrganizer = Role.Equals("organizer", StringComparison.InvariantCultureIgnoreCase); Claims = claims ?? new Dictionary(); Name = Claims.TryGetValue("name", out var name) ? name : string.Empty; Email = Claims.TryGetValue("email", out var email) ? email : string.Empty; diff --git a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Infrastructure/Mongo/Documents/Extensions.cs b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Infrastructure/Mongo/Documents/Extensions.cs index 8b6729bd1..d3b201985 100644 --- a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Infrastructure/Mongo/Documents/Extensions.cs +++ b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Infrastructure/Mongo/Documents/Extensions.cs @@ -10,10 +10,9 @@ namespace MiniSpace.Services.Identity.Infrastructure.Mongo.Documents internal static class Extensions { public static User AsEntity(this UserDocument document) - => new User(document.Id, document.Name, document.Email, document.Password, - Enum.Parse(document.Role, true), document.CreatedAt, - document.Permissions - ) + { + var user = new User(document.Id, document.Name, document.Email, document.Password, + Enum.Parse(document.Role, true), document.CreatedAt, document.Permissions) { IsEmailVerified = document.IsEmailVerified, EmailVerificationToken = document.EmailVerificationToken, @@ -22,6 +21,12 @@ public static User AsEntity(this UserDocument document) TwoFactorSecret = document.TwoFactorSecret }; + user.SetOnlineStatus(document.IsOnline, document.DeviceType); + user.UpdateLastActive(); + + return user; + } + public static UserDocument AsDocument(this User entity) => new UserDocument { @@ -36,7 +41,10 @@ public static UserDocument AsDocument(this User entity) EmailVerificationToken = entity.EmailVerificationToken, EmailVerifiedAt = entity.EmailVerifiedAt, IsTwoFactorEnabled = entity.IsTwoFactorEnabled, - TwoFactorSecret = entity.TwoFactorSecret + TwoFactorSecret = entity.TwoFactorSecret, + IsOnline = entity.IsOnline, + DeviceType = entity.DeviceType, + LastActive = entity.LastActive }; public static UserDto AsDto(this UserDocument document) @@ -51,7 +59,10 @@ public static UserDto AsDto(this UserDocument document) IsEmailVerified = document.IsEmailVerified, EmailVerifiedAt = document.EmailVerifiedAt, IsTwoFactorEnabled = document.IsTwoFactorEnabled, - TwoFactorSecret = document.TwoFactorSecret + TwoFactorSecret = document.TwoFactorSecret, + IsOnline = document.IsOnline, + DeviceType = document.DeviceType, + LastActive = document.LastActive }; public static RefreshToken AsEntity(this RefreshTokenDocument document) diff --git a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Infrastructure/Mongo/Documents/UserDocument.cs b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Infrastructure/Mongo/Documents/UserDocument.cs index 2ecf2fc72..ebf458716 100644 --- a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Infrastructure/Mongo/Documents/UserDocument.cs +++ b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Infrastructure/Mongo/Documents/UserDocument.cs @@ -20,5 +20,9 @@ internal sealed class UserDocument : IIdentifiable public DateTime? EmailVerifiedAt { get; set; } public bool IsTwoFactorEnabled { get; set; } public string TwoFactorSecret { get; set; } + + public bool IsOnline { get; set; } + public string DeviceType { get; set; } + public DateTime? LastActive { get; set; } } } diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Api/Program.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Api/Program.cs index 4b3d09f62..2020f659e 100644 --- a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Api/Program.cs +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Api/Program.cs @@ -38,12 +38,14 @@ public static async Task Main(string[] args) .Get>("organizations/{organizationId}/children") .Get>("organizations/{organizationId}/children/all") // the organizations users is the organizer - .Get>("users/{userId}/organizations") + .Get>("organizations/users/{userId}/organizations") // organizations, user is a part of - .Get>("users/{userId}/organizations/follow") + .Get>("organizations/users/{userId}/organizations/follow") .Get("organizations/{organizationId}/details/gallery-users") .Get>("organizations/{organizationId}/roles") .Get>("organizations/paginated") + .Get>("organizations/{organizationId}/requests") + .Post("organizations", afterDispatch: (cmd, ctx) => ctx.Response.Created($"organizations/{cmd.OrganizationId}")) diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/Handlers/FollowOrganizationHandler.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/Handlers/FollowOrganizationHandler.cs index 2ae4a28ad..5eeed3c65 100644 --- a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/Handlers/FollowOrganizationHandler.cs +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/Handlers/FollowOrganizationHandler.cs @@ -75,7 +75,6 @@ public async Task HandleAsync(FollowOrganization command, CancellationToken canc var newUser = new User(command.UserId, defaultRole); await _organizationMembersRepository.AddMemberAsync(command.OrganizationId, newUser); - // Add the organization to the user's list of organizations await _userOrganizationsRepository.AddOrganizationToUserAsync(command.UserId, command.OrganizationId); // Publish event diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Dto/OrganizationDto.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Dto/OrganizationDto.cs index a5e0b36ed..d4dbff79e 100644 --- a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Dto/OrganizationDto.cs +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Dto/OrganizationDto.cs @@ -23,6 +23,7 @@ public class OrganizationDto public string Email { get; set; } public IEnumerable Users { get; set; } + public OrganizationSettingsDto Settings { get; set; } public OrganizationDto() { @@ -47,6 +48,7 @@ public OrganizationDto(Organization organization) // Convert each User entity to a UserDto Users = organization.Users?.Select(user => new UserDto(user)).ToList(); + Settings = organization.Settings != null ? new OrganizationSettingsDto(organization.Settings) : null; } } } diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Dto/OrganizationRequestDto.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Dto/OrganizationRequestDto.cs new file mode 100644 index 000000000..f02c36f01 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Dto/OrganizationRequestDto.cs @@ -0,0 +1,27 @@ +using System; +using MiniSpace.Services.Organizations.Core.Entities; + +namespace MiniSpace.Services.Organizations.Application.DTO +{ + public class OrganizationRequestDto + { + public Guid RequestId { get; set; } + public Guid UserId { get; set; } + public DateTime RequestDate { get; set; } + public string State { get; set; } + public string Reason { get; set; } + + // Factory method to create a DTO from an OrganizationRequest entity + public static OrganizationRequestDto FromEntity(OrganizationRequest request) + { + return new OrganizationRequestDto + { + RequestId = request.Id, + UserId = request.UserId, + RequestDate = request.RequestDate, + State = request.State.ToString(), + Reason = request.Reason + }; + } + } +} diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Dto/OrganizationRequestsDto.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Dto/OrganizationRequestsDto.cs new file mode 100644 index 000000000..58180e0cc --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Dto/OrganizationRequestsDto.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using MiniSpace.Services.Organizations.Core.Entities; + +namespace MiniSpace.Services.Organizations.Application.DTO +{ + public class OrganizationRequestsDto + { + public Guid OrganizationId { get; set; } + public IEnumerable Requests { get; set; } + + public OrganizationRequestsDto() + { + Requests = new List(); + } + + public static OrganizationRequestsDto FromEntity(Guid organizationId, IEnumerable requests) + { + var requestDtos = new List(); + foreach (var request in requests) + { + requestDtos.Add(OrganizationRequestDto.FromEntity(request)); + } + + return new OrganizationRequestsDto + { + OrganizationId = organizationId, + Requests = requestDtos + }; + } + } +} diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Queries/GetPaginatedOrganizationRequests.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Queries/GetPaginatedOrganizationRequests.cs new file mode 100644 index 000000000..053e4eee9 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Queries/GetPaginatedOrganizationRequests.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Convey.CQRS.Queries; +using MiniSpace.Services.Organizations.Application.DTO; + +namespace MiniSpace.Services.Organizations.Application.Queries +{ + public class GetPaginatedOrganizationRequests : IQuery> + { + public Guid OrganizationId { get; } + public int Page { get; } + public int PageSize { get; } + + public GetPaginatedOrganizationRequests(Guid organizationId, int page, int pageSize) + { + OrganizationId = organizationId; + Page = page; + PageSize = pageSize; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Queries/GetUserCreatedOrganizations.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Queries/GetUserCreatedOrganizations.cs new file mode 100644 index 000000000..a9cba871f --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Queries/GetUserCreatedOrganizations.cs @@ -0,0 +1,20 @@ +using Convey.CQRS.Queries; +using MiniSpace.Services.Organizations.Application.DTO; +using System; + +namespace MiniSpace.Services.Organizations.Application.Queries +{ + public class GetUserCreatedOrganizations : IQuery> + { + public Guid UserId { get; } + public int Page { get; } + public int PageSize { get; } + + public GetUserCreatedOrganizations(Guid userId, int page, int pageSize) + { + UserId = userId; + Page = page; + PageSize = pageSize; + } + } +} diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Entities/OrganizationView.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Entities/OrganizationView.cs new file mode 100644 index 000000000..410d92b1e --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Entities/OrganizationView.cs @@ -0,0 +1,22 @@ +using System; + +namespace MiniSpace.Services.Organizations.Core.Entities +{ + public class OrganizationView + { + public Guid UserId { get; private set; } + public DateTime Date { get; private set; } + public string IpAddress { get; private set; } + public string DeviceType { get; private set; } + public string OperatingSystem { get; private set; } + + public OrganizationView(Guid userId, DateTime date, string ipAddress, string deviceType, string operatingSystem) + { + UserId = userId; + Date = date; + IpAddress = ipAddress; + DeviceType = deviceType; + OperatingSystem = operatingSystem; + } + } +} diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Entities/OrganizationViews.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Entities/OrganizationViews.cs new file mode 100644 index 000000000..0f1e744a4 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Entities/OrganizationViews.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace MiniSpace.Services.Organizations.Core.Entities +{ + public class OrganizationViews + { + public Guid OrganizationId { get; private set; } + public IEnumerable Views { get; private set; } + + public OrganizationViews(Guid organizationId, IEnumerable views) + { + OrganizationId = organizationId; + Views = views ?? new List(); + } + + public void AddView(Guid userId, DateTime date, string ipAddress, string deviceType, string operatingSystem) + { + var viewList = new List(Views) + { + new OrganizationView(userId, date, ipAddress, deviceType, operatingSystem) + }; + Views = viewList; + } + + public void RemoveView(Guid userId) + { + var viewList = new List(Views); + var viewToRemove = viewList.Find(view => view.UserId == userId); + if (viewToRemove != null) + { + viewList.Remove(viewToRemove); + Views = viewList; + } + } + } +} diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Entities/UserOrganizationsViews.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Entities/UserOrganizationsViews.cs new file mode 100644 index 000000000..13b5a5f84 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Entities/UserOrganizationsViews.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace MiniSpace.Services.Organizations.Core.Entities +{ + public class UserOrganizationsViews + { + public Guid UserId { get; private set; } + public IEnumerable Views { get; private set; } + + public UserOrganizationsViews(Guid userProfileId, IEnumerable views) + { + UserId = userProfileId; + Views = views ?? new List(); + } + + public void AddView(Guid organizationId, DateTime date, string ipAddress, string deviceType, string operatingSystem) + { + var viewList = new List(Views) + { + new UserView(organizationId, date, ipAddress, deviceType, operatingSystem) + }; + Views = viewList; + } + + public void RemoveView(Guid organizationId) + { + var viewList = new List(Views); + var viewToRemove = viewList.Find(view => view.OrganizationId == organizationId); + if (viewToRemove != null) + { + viewList.Remove(viewToRemove); + Views = viewList; + } + } + } +} diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Entities/UserView.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Entities/UserView.cs new file mode 100644 index 000000000..25ff65882 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Entities/UserView.cs @@ -0,0 +1,22 @@ +using System; + +namespace MiniSpace.Services.Organizations.Core.Entities +{ + public class UserView + { + public Guid OrganizationId { get; private set; } + public DateTime Date { get; private set; } + public string IpAddress { get; private set; } + public string DeviceType { get; private set; } + public string OperatingSystem { get; private set; } + + public UserView(Guid organizationId, DateTime date, string ipAddress, string deviceType, string operatingSystem) + { + OrganizationId = organizationId; + Date = date; + IpAddress = ipAddress; + DeviceType = deviceType; + OperatingSystem = operatingSystem; + } + } +} diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Repositories/IOrganizationViewsRepository.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Repositories/IOrganizationViewsRepository.cs new file mode 100644 index 000000000..bb27e7881 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Repositories/IOrganizationViewsRepository.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using MiniSpace.Services.Organizations.Core.Entities; + +namespace MiniSpace.Services.Organizations.Core.Repositories +{ + public interface IOrganizationViewsRepository + { + Task GetAsync(Guid organizationId); + Task AddAsync(OrganizationViews organizationViews); + Task UpdateAsync(OrganizationViews organizationViews); + Task DeleteAsync(Guid organizationId); + } +} diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Repositories/IUserOrganizationViewsRepository.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Repositories/IUserOrganizationViewsRepository.cs new file mode 100644 index 000000000..c3c0d99fe --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Repositories/IUserOrganizationViewsRepository.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using MiniSpace.Services.Organizations.Core.Entities; + +namespace MiniSpace.Services.Organizations.Core.Repositories +{ + public interface IUserOrganizationViewsRepository + { + Task GetAsync(Guid userId); + Task AddAsync(UserOrganizationsViews userOrganizationsViews); + Task UpdateAsync(UserOrganizationsViews userOrganizationsViews); + Task DeleteAsync(Guid userId); + } +} diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Queries/Handlers/GetPaginatedOrganizationRequestsHandler.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Queries/Handlers/GetPaginatedOrganizationRequestsHandler.cs new file mode 100644 index 000000000..92308c268 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Queries/Handlers/GetPaginatedOrganizationRequestsHandler.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Convey.CQRS.Queries; +using MiniSpace.Services.Organizations.Application.DTO; +using MiniSpace.Services.Organizations.Application.Queries; +using MiniSpace.Services.Organizations.Core.Repositories; +using MiniSpace.Services.Organizations.Infrastructure.Mongo.Documents; +using MiniSpace.Services.Organizations.Infrastructure.Mongo.Repositories; +using MongoDB.Driver; + +namespace MiniSpace.Services.Organizations.Infrastructure.Queries.Handlers +{ + public class GetPaginatedOrganizationRequestsHandler : IQueryHandler> + { + private readonly IOrganizationRequestsRepository _requestsRepository; + + public GetPaginatedOrganizationRequestsHandler(IOrganizationRequestsRepository requestsRepository) + { + _requestsRepository = requestsRepository; + } + + public async Task> HandleAsync(GetPaginatedOrganizationRequests query, CancellationToken cancellationToken) + { + // Retrieve all requests for the organization + var requests = await _requestsRepository.GetRequestsAsync(query.OrganizationId); + + if (!requests.Any()) + { + return new MiniSpace.Services.Organizations.Application.DTO.PagedResult(Enumerable.Empty(), query.Page, query.PageSize, 0); + } + + // Count total items + var totalItems = requests.Count(); + + // Paginate the requests + var paginatedRequests = requests + .Skip((query.Page - 1) * query.PageSize) + .Take(query.PageSize) + .Select(r => new OrganizationRequestDto + { + RequestId = r.Id, + UserId = r.UserId, + RequestDate = r.RequestDate, + State = r.State.ToString(), + Reason = r.Reason + }) + .ToList(); + + // Return a paged result + return new MiniSpace.Services.Organizations.Application.DTO.PagedResult(paginatedRequests, query.Page, query.PageSize, totalItems); + } + } +} diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Queries/Handlers/GetPaginatedOrganizationsHandler.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Queries/Handlers/GetPaginatedOrganizationsHandler.cs index 29e3d8878..f31a57baf 100644 --- a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Queries/Handlers/GetPaginatedOrganizationsHandler.cs +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Queries/Handlers/GetPaginatedOrganizationsHandler.cs @@ -115,7 +115,8 @@ private async Task ConvertToDtoAsync(OrganizationDocument organ City = organization.City, Telephone = organization.Telephone, Email = organization.Email, - Users = members?.Select(user => new UserDto(user)).ToList() + Users = members?.Select(user => new UserDto(user)).ToList(), + Settings = organization.Settings != null ? new OrganizationSettingsDto(organization.Settings) : null }; } } diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Queries/Handlers/GetPaginatedUserOrganizationsHandler.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Queries/Handlers/GetPaginatedUserOrganizationsHandler.cs index e86aecb3c..616b84d93 100644 --- a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Queries/Handlers/GetPaginatedUserOrganizationsHandler.cs +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Queries/Handlers/GetPaginatedUserOrganizationsHandler.cs @@ -72,6 +72,7 @@ private async Task ConvertToDtoAsync(Organization organization) Telephone = organization.Telephone, Email = organization.Email, Users = members?.Select(user => new UserDto(user)).ToList(), + Settings = organization.Settings != null ? new OrganizationSettingsDto(organization.Settings) : null // Include settings }; } } diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Queries/Handlers/GetUserFollowOrganizationsHandler.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Queries/Handlers/GetUserFollowOrganizationsHandler.cs index 30a4e4571..0b05cb375 100644 --- a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Queries/Handlers/GetUserFollowOrganizationsHandler.cs +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Queries/Handlers/GetUserFollowOrganizationsHandler.cs @@ -34,6 +34,11 @@ public async Task> HandleAsync(GetUserF var organizationIds = await _userOrganizationsRepository.GetUserOrganizationsAsync(query.UserId); var organizationDetailsList = new List(); + if (organizationIds == null || !organizationIds.Any()) + { + return Enumerable.Empty(); + } + foreach (var organizationId in organizationIds) { var organization = await _organizationRepository.GetAsync(organizationId); diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Repositories/OrganizationMembersMongoRepository.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Repositories/OrganizationMembersMongoRepository.cs index 20e2d5e96..db8a2e443 100644 --- a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Repositories/OrganizationMembersMongoRepository.cs +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Repositories/OrganizationMembersMongoRepository.cs @@ -86,5 +86,6 @@ public async Task DeleteMemberAsync(Guid organizationId, Guid memberId) } } } + } } diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Api/Program.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Api/Program.cs index bae6998ba..a7b91bf1b 100644 --- a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Api/Program.cs +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Api/Program.cs @@ -14,6 +14,7 @@ using MiniSpace.Services.Posts.Application; using MiniSpace.Services.Posts.Application.Commands; using MiniSpace.Services.Posts.Application.Dto; +using MiniSpace.Services.Posts.Application.DTO; using MiniSpace.Services.Posts.Application.Queries; using MiniSpace.Services.Posts.Application.Services; using MiniSpace.Services.Posts.Core.Wrappers; @@ -41,6 +42,9 @@ public static async Task Main(string[] args) .Get>("posts/search") .Get>("posts/users/{userId}/feed") .Get>("posts/organizer/{organizerId}") + .Post("posts/{postId}/view", + afterDispatch: (cmd, ctx) => ctx.Response.NoContent()) + .Get>("posts/users/{userId}/views") .Put("posts/{postId}") .Delete("posts/{postId}") .Post("posts", diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Commands/Handlers/ViewPostHandler.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Commands/Handlers/ViewPostHandler.cs new file mode 100644 index 000000000..5beee8aec --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Commands/Handlers/ViewPostHandler.cs @@ -0,0 +1,56 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Convey.CQRS.Commands; +using MiniSpace.Services.Posts.Core.Entities; +using MiniSpace.Services.Posts.Core.Repositories; +using Microsoft.Extensions.Logging; + +namespace MiniSpace.Services.Posts.Application.Commands.Handlers +{ + public class ViewPostHandler : ICommandHandler + { + private readonly IPostsUserViewsRepository _postsUserViewsRepository; + private readonly IPostRepository _postRepository; + private readonly ILogger _logger; + + public ViewPostHandler( + IPostsUserViewsRepository postsUserViewsRepository, + IPostRepository postRepository, + ILogger logger) + { + _postsUserViewsRepository = postsUserViewsRepository; + _postRepository = postRepository; + _logger = logger; + } + + public async Task HandleAsync(ViewPost command, CancellationToken cancellationToken) + { + var postExists = await _postRepository.ExistsAsync(command.PostId); + if (!postExists) + { + _logger.LogWarning($"Post with ID {command.PostId} not found."); + return; + } + + var userViews = await _postsUserViewsRepository.GetAsync(command.UserId); + if (userViews == null) + { + userViews = new PostsViews(command.UserId, Enumerable.Empty()); + } + + var existingView = userViews.Views.FirstOrDefault(v => v.PostId == command.PostId); + if (existingView != null) + { + userViews.RemoveView(command.PostId); + } + + userViews.AddView(command.PostId, DateTime.UtcNow); + + await _postsUserViewsRepository.UpdateAsync(userViews); + + _logger.LogInformation($"User {command.UserId} viewed post {command.PostId}."); + } + } +} diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Commands/ViewPost.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Commands/ViewPost.cs new file mode 100644 index 000000000..494c2cbe0 --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Commands/ViewPost.cs @@ -0,0 +1,17 @@ +using System; +using Convey.CQRS.Commands; + +namespace MiniSpace.Services.Posts.Application.Commands +{ + public class ViewPost : ICommand + { + public Guid UserId { get; } + public Guid PostId { get; } + + public ViewPost(Guid userId, Guid postId) + { + UserId = userId; + PostId = postId; + } + } +} diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Dto/UserPostsViewsDto.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Dto/UserPostsViewsDto.cs new file mode 100644 index 000000000..c030f30ab --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Dto/UserPostsViewsDto.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; + +namespace MiniSpace.Services.Posts.Application.DTO +{ + public class UserPostsViewsDto + { + public Guid UserId { get; set; } + public IEnumerable Views { get; set; } + } +} diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Dto/ViewDto.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Dto/ViewDto.cs new file mode 100644 index 000000000..1f307795a --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Dto/ViewDto.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; + +namespace MiniSpace.Services.Posts.Application.DTO +{ + public class ViewDto + { + public Guid PostId { get; set; } + public DateTime Date { get; set; } + } +} diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Events/External/Handlers/CommentCreatedHandler.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Events/External/Handlers/CommentCreatedHandler.cs index 170b02832..473fbece3 100644 --- a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Events/External/Handlers/CommentCreatedHandler.cs +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Events/External/Handlers/CommentCreatedHandler.cs @@ -27,7 +27,6 @@ public async Task HandleAsync(CommentCreated @event, CancellationToken cancellat Console.WriteLine("Received CommentCreated event:"); Console.WriteLine(eventJson); - // Create a new Comment entity based on the received event data var comment = new Comment( @event.CommentId, @event.ContextId, @@ -41,7 +40,6 @@ public async Task HandleAsync(CommentCreated @event, CancellationToken cancellat @event.IsDeleted ); - // Save the comment to the user's comment history await _userCommentsHistoryRepository.SaveCommentAsync(@event.UserId, comment); } } diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Queries/GetUserPostViews.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Queries/GetUserPostViews.cs new file mode 100644 index 000000000..98dbb78cb --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Queries/GetUserPostViews.cs @@ -0,0 +1,22 @@ +using System; +using Convey.CQRS.Queries; +using MiniSpace.Services.Posts.Application.Dto; +using MiniSpace.Services.Posts.Application.DTO; +using MiniSpace.Services.Posts.Core.Wrappers; + +namespace MiniSpace.Services.Posts.Application.Queries +{ + public class GetUserPostViews : IQuery> + { + public Guid UserId { get; } + public int PageNumber { get; } + public int PageSize { get; } + + public GetUserPostViews(Guid userId, int pageNumber = 1, int pageSize = 10) + { + UserId = userId; + PageNumber = pageNumber; + PageSize = pageSize; + } + } +} diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Entities/Post.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Entities/Post.cs index 0dc5b9df5..2870b48f0 100644 --- a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Entities/Post.cs +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Entities/Post.cs @@ -18,7 +18,7 @@ public class Post : AggregateRoot public DateTime CreatedAt { get; private set; } public DateTime? UpdatedAt { get; private set; } public PostContext Context { get; private set; } - public VisibilityStatus Visibility { get; private set; } // New visibility status property + public VisibilityStatus Visibility { get; private set; } public Post(Guid id, Guid? userId, Guid? organizationId, Guid? eventId, string textContent, IEnumerable mediaFiles, DateTime createdAt, State state, PostContext context, DateTime? publishDate, @@ -166,7 +166,6 @@ public void RemoveMediaFile(string mediaFileUrl, DateTime now) MediaFiles = MediaFiles.Where(mf => mf != mediaFileUrl).ToList(); UpdatedAt = now; - // Raise an event if necessary (e.g., PostMediaFileRemovedEvent) } private static void CheckTextContent(AggregateId id, string textContent) diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Entities/PostsViews.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Entities/PostsViews.cs new file mode 100644 index 000000000..af833c8ff --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Entities/PostsViews.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; + +namespace MiniSpace.Services.Posts.Core.Entities +{ + public class PostsViews + { + public Guid UserId { get; private set; } + public IEnumerable Views { get; private set; } + + public PostsViews(Guid userId, IEnumerable views) + { + UserId = userId; + Views = views ?? new List(); + } + + public void AddView(Guid postId, DateTime date) + { + var viewList = new List(Views) + { + new View(postId, date) + }; + Views = viewList; + } + + public void RemoveView(Guid postId) + { + var viewList = new List(Views); + var viewToRemove = viewList.Find(view => view.PostId == postId); + if (viewToRemove != null) + { + viewList.Remove(viewToRemove); + Views = viewList; + } + } + } +} diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Entities/View.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Entities/View.cs new file mode 100644 index 000000000..0fcbb0d57 --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Entities/View.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace MiniSpace.Services.Posts.Core.Entities +{ + public class View + { + public Guid PostId { get; private set; } + public DateTime Date { get; private set; } + + public View(Guid postId, DateTime date) + { + PostId = postId; + Date = date; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Repositories/IPostsUserViewsRepository.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Repositories/IPostsUserViewsRepository.cs new file mode 100644 index 000000000..aaf0548fc --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Repositories/IPostsUserViewsRepository.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using MiniSpace.Services.Posts.Core.Entities; + +namespace MiniSpace.Services.Posts.Core.Repositories +{ + public interface IPostsUserViewsRepository + { + Task GetAsync(Guid userId); + Task AddAsync(PostsViews postsViews); + Task UpdateAsync(PostsViews postsViews); + Task DeleteAsync(Guid userId); + } +} diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Repositories/IUserCommentsHistoryRepository.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Repositories/IUserCommentsHistoryRepository.cs index e82c0c15f..cfc5b0479 100644 --- a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Repositories/IUserCommentsHistoryRepository.cs +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Repositories/IUserCommentsHistoryRepository.cs @@ -8,36 +8,12 @@ namespace MiniSpace.Services.Posts.Core.Repositories { public interface IUserCommentsHistoryRepository { - /// - /// Saves a comment to the user's comment history. - /// - /// The ID of the user. - /// The comment to be saved. - /// A task representing the asynchronous operation. Task SaveCommentAsync(Guid userId, Comment comment); - /// - /// Retrieves all comments created by a specific user. - /// - /// The ID of the user. - /// A task representing the asynchronous operation, with a result of a list of comments. Task> GetUserCommentsAsync(Guid userId); - /// - /// Retrieves a paged response of comments created by a specific user. - /// - /// The ID of the user. - /// The page number to retrieve. - /// The number of comments per page. - /// A task representing the asynchronous operation, with a result of a paged response of comments. Task> GetUserCommentsPagedAsync(Guid userId, int pageNumber, int pageSize); - /// - /// Deletes a specific comment from the user's comment history. - /// - /// The ID of the user. - /// The ID of the comment to be deleted. - /// A task representing the asynchronous operation. Task DeleteCommentAsync(Guid userId, Guid commentId); } } diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Repositories/IUserReactionsHistoryRepository.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Repositories/IUserReactionsHistoryRepository.cs index 39a03a855..d2d65ae0f 100644 --- a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Repositories/IUserReactionsHistoryRepository.cs +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Repositories/IUserReactionsHistoryRepository.cs @@ -8,36 +8,12 @@ namespace MiniSpace.Services.Posts.Core.Repositories { public interface IUserReactionsHistoryRepository { - /// - /// Saves a reaction to the user's reaction history. - /// - /// The ID of the user. - /// The reaction to be saved. - /// A task representing the asynchronous operation. Task SaveReactionAsync(Guid userId, Reaction reaction); - /// - /// Retrieves all reactions created by a specific user. - /// - /// The ID of the user. - /// A task representing the asynchronous operation, with a result of a list of reactions. Task> GetUserReactionsAsync(Guid userId); - /// - /// Retrieves a paged response of reactions created by a specific user. - /// - /// The ID of the user. - /// The page number to retrieve. - /// The number of reactions per page. - /// A task representing the asynchronous operation, with a result of a paged response of reactions. Task> GetUserReactionsPagedAsync(Guid userId, int pageNumber, int pageSize); - - /// - /// Deletes a specific reaction from the user's reaction history. - /// - /// The ID of the user. - /// The ID of the reaction to be deleted. - /// A task representing the asynchronous operation. + Task DeleteReactionAsync(Guid userId, Guid reactionId); } } diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Extensions.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Extensions.cs index 1909891bd..1e96b74bc 100644 --- a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Extensions.cs +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Extensions.cs @@ -43,6 +43,7 @@ using MiniSpace.Services.Events.Infrastructure.Services.Clients; using MiniSpace.Services.Posts.Application.Services.Clients; using Microsoft.ML; +using MiniSpace.Services.Events.Infrastructure.Mongo.Repositories; namespace MiniSpace.Services.Posts.Infrastructure { @@ -64,6 +65,7 @@ public static IConveyBuilder AddInfrastructure(this IConveyBuilder builder) builder.Services.AddTransient(); builder.Services.AddTransient(); builder.Services.AddTransient(); + builder.Services.AddTransient(); builder.Services.AddSingleton(new MLContext()); builder.Services.AddTransient(); @@ -96,6 +98,7 @@ public static IConveyBuilder AddInfrastructure(this IConveyBuilder builder) .AddMongoRepository("posts") .AddMongoRepository("user_comments_history") .AddMongoRepository("user_reactions_history") + .AddMongoRepository("user_views") .AddWebApiSwaggerDocs() .AddCertificateAuthentication() .AddSecurity(); diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Documents/PostsViewsExtensions.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Documents/PostsViewsExtensions.cs new file mode 100644 index 000000000..b3b506e21 --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Documents/PostsViewsExtensions.cs @@ -0,0 +1,41 @@ +using System; +using System.Linq; +using MiniSpace.Services.Posts.Application.DTO; +using MiniSpace.Services.Posts.Core.Entities; + +namespace MiniSpace.Services.Posts.Infrastructure.Mongo.Documents +{ + public static class PostsViewsExtensions + { + public static UserPostsViewsDto AsDto(this UserPostsViewsDocument document) + { + return new UserPostsViewsDto + { + UserId = document.UserId, + Views = document.Views.Select(v => new ViewDto + { + PostId = v.PostId, + Date = v.Date + }) + }; + } + + public static UserPostsViewsDocument AsDocument(this PostsViews entity) + { + return new UserPostsViewsDocument + { + Id = Guid.NewGuid(), + UserId = entity.UserId, + Views = entity.Views.Select(ViewDocument.FromEntity).ToList() + }; + } + + public static PostsViews AsEntity(this UserPostsViewsDocument document) + { + return new PostsViews( + document.UserId, + document.Views.Select(v => new View(v.PostId, v.Date)) + ); + } + } +} diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Documents/UserPostsViewsDocument.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Documents/UserPostsViewsDocument.cs new file mode 100644 index 000000000..923215369 --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Documents/UserPostsViewsDocument.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Convey.Types; +using MiniSpace.Services.Posts.Core.Entities; + +namespace MiniSpace.Services.Posts.Infrastructure.Mongo.Documents +{ + public class UserPostsViewsDocument : IIdentifiable + { + public Guid Id { get; set; } + public Guid UserId { get; set; } + public List Views { get; set; } = new List(); + + public static UserPostsViewsDocument FromEntity(PostsViews eventsViews) + { + return new UserPostsViewsDocument + { + Id = Guid.NewGuid(), + UserId = eventsViews.UserId, + Views = new List(eventsViews.Views.Select(ViewDocument.FromEntity)) + }; + } + + public PostsViews ToEntity() + { + return new PostsViews(UserId, Views.Select(view => view.ToEntity())); + } + } +} diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Documents/ViewDocument.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Documents/ViewDocument.cs new file mode 100644 index 000000000..279f3aa25 --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Documents/ViewDocument.cs @@ -0,0 +1,28 @@ +using System; +using Convey.Types; +using MiniSpace.Services.Posts.Core.Entities; + +namespace MiniSpace.Services.Posts.Infrastructure.Mongo.Documents +{ + public class ViewDocument : IIdentifiable + { + public Guid Id { get; set; } + public Guid PostId { get; set; } + public DateTime Date { get; set; } + + public static ViewDocument FromEntity(View view) + { + return new ViewDocument + { + Id = Guid.NewGuid(), + PostId = view.PostId, + Date = view.Date + }; + } + + public View ToEntity() + { + return new View(PostId, Date); + } + } +} diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Queries/Handlers/GetUserFeedHandler.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Queries/Handlers/GetUserFeedHandler.cs index c6e4095cb..2d0ed7d7f 100644 --- a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Queries/Handlers/GetUserFeedHandler.cs +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Queries/Handlers/GetUserFeedHandler.cs @@ -46,10 +46,8 @@ public async Task> HandleAsync(GetUserFeed query, Cancell { _logger.LogInformation("Handling GetUserFeed query: {Query}", JsonConvert.SerializeObject(query)); - // Retrieve the user details var user = await _studentsServiceClient.GetStudentByIdAsync(query.UserId); - // Step 1: Retrieve all posts without pagination (we'll handle ranking and pagination manually) var allPostsRequest = new BrowseRequest { SortBy = new List { query.SortBy }, @@ -63,63 +61,69 @@ public async Task> HandleAsync(GetUserFeed query, Cancell return new PagedResponse(Enumerable.Empty(), query.PageNumber, query.PageSize, 0); } - // Step 2: Retrieve user interactions (comments and reactions) var userComments = await _userCommentsHistoryRepository.GetUserCommentsAsync(query.UserId); var userReactions = await _userReactionsHistoryRepository.GetUserReactionsAsync(query.UserId); - // Step 3: Analyze user interactions to infer interests and calculate coefficients, including user's interests and languages var userInterests = AnalyzeUserInteractions(user, userComments, userReactions); - // Step 4: Rank all posts by relevance to user interests - var rankedPosts = await _postRecommendationService.RankPostsByUserInterestAsync(query.UserId, allPostsResult.Items, userInterests); + IEnumerable<(PostDto Post, double Score)> rankedPosts; + + if (!userInterests.Any() && !userComments.Any() && !userReactions.Any()) + { + _logger.LogInformation("User {UserId} has no interactions or defined interests, generating a random feed.", query.UserId); + rankedPosts = GenerateRandomFeed(allPostsResult.Items); + } + else + { + rankedPosts = await _postRecommendationService.RankPostsByUserInterestAsync(query.UserId, allPostsResult.Items, userInterests); + } - // Step 5: Combine ranked posts with remaining posts, prioritizing more relevant posts var combinedPosts = CombineRankedAndUnrankedPosts(rankedPosts, allPostsResult.Items); - // Step 6: Paginate the combined posts var pagedPosts = combinedPosts .Skip((query.PageNumber - 1) * query.PageSize) .Take(query.PageSize) .ToList(); - // Log the result _logger.LogInformation("User {UserId} feed generated with {PostCount} posts.", query.UserId, pagedPosts.Count); return new PagedResponse(pagedPosts, query.PageNumber, query.PageSize, combinedPosts.Count()); } - private IDictionary AnalyzeUserInteractions(UserDto user, IEnumerable userComments, IEnumerable userReactions) { var interestKeywords = new Dictionary(); - // Include user interests in the analysis - foreach (var interest in user.Interests) + if (user.Interests != null) { - if (interestKeywords.ContainsKey(interest)) + foreach (var interest in user.Interests) { - interestKeywords[interest] += 1; - } - else - { - interestKeywords[interest] = 1; + if (interestKeywords.ContainsKey(interest)) + { + interestKeywords[interest] += 1; + } + else + { + interestKeywords[interest] = 1; + } } } - // Include user languages in the analysis - foreach (var language in user.Languages) + if (user.Languages != null) { - if (interestKeywords.ContainsKey(language)) - { - interestKeywords[language] += 1; - } - else + foreach (var language in user.Languages) { - interestKeywords[language] = 1; + if (interestKeywords.ContainsKey(language)) + { + interestKeywords[language] += 1; + } + else + { + interestKeywords[language] = 1; + } } } - // Analyze user comments foreach (var comment in userComments) { var words = comment.TextContent.Split(' ', StringSplitOptions.RemoveEmptyEntries); @@ -136,7 +140,6 @@ private IDictionary AnalyzeUserInteractions(UserDto user, IEnume } } - // Analyze user reactions foreach (var reaction in userReactions) { var reactionType = reaction.Type; @@ -150,24 +153,34 @@ private IDictionary AnalyzeUserInteractions(UserDto user, IEnume } } - // Normalize coefficients to sum to 1 var total = interestKeywords.Values.Sum(); - var normalizedInterests = interestKeywords.ToDictionary(kvp => kvp.Key, kvp => kvp.Value / total); + if (total > 0) + { + var normalizedInterests = interestKeywords.ToDictionary(kvp => kvp.Key, kvp => kvp.Value / total); + _logger.LogInformation("Inferred user interests with coefficients: {Interests}", string.Join(", ", normalizedInterests.Select(kvp => $"{kvp.Key}: {kvp.Value:F2}"))); + return normalizedInterests; + } - _logger.LogInformation("Inferred user interests with coefficients: {Interests}", string.Join(", ", normalizedInterests.Select(kvp => $"{kvp.Key}: {kvp.Value:F2}"))); + return interestKeywords; + } - return normalizedInterests; + private IEnumerable<(PostDto Post, double Score)> GenerateRandomFeed(IEnumerable allPosts) + { + var random = new Random(); + return allPosts + .OrderBy(p => random.NextDouble()) + .Select(p => (p, Score: random.NextDouble())) + .Take(100); } private IEnumerable CombineRankedAndUnrankedPosts(IEnumerable<(PostDto Post, double Score)> rankedPosts, IEnumerable allPosts) { - // Order posts by the relevance score first and then by publish date if the score is equal var rankedPostIds = rankedPosts.Select(rp => rp.Post.Id).ToHashSet(); var unrankedPosts = allPosts.Where(p => !rankedPostIds.Contains(p.Id)); return rankedPosts.Select(rp => rp.Post) - .Concat(unrankedPosts.OrderByDescending(p => p.PublishDate)); // Fallback to publish date for unranked posts + .Concat(unrankedPosts.OrderByDescending(p => p.PublishDate)); } } } diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Queries/Handlers/GetUserPostViewsHandler.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Queries/Handlers/GetUserPostViewsHandler.cs new file mode 100644 index 000000000..f0cda20ee --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Queries/Handlers/GetUserPostViewsHandler.cs @@ -0,0 +1,44 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Convey.CQRS.Queries; +using MiniSpace.Services.Posts.Application.Dto; +using MiniSpace.Services.Posts.Application.DTO; +using MiniSpace.Services.Posts.Application.Queries; +using MiniSpace.Services.Posts.Core.Repositories; +using MiniSpace.Services.Posts.Core.Wrappers; + +namespace MiniSpace.Services.Posts.Infrastructure.Mongo.Queries.Handlers +{ + public class GetUserPostViewsHandler : IQueryHandler> + { + private readonly IPostsUserViewsRepository _postsUserViewsRepository; + + public GetUserPostViewsHandler(IPostsUserViewsRepository postsUserViewsRepository) + { + _postsUserViewsRepository = postsUserViewsRepository; + } + + public async Task> HandleAsync(GetUserPostViews query, CancellationToken cancellationToken) + { + // Retrieve the user's post views + var userViews = await _postsUserViewsRepository.GetAsync(query.UserId); + + if (userViews == null || !userViews.Views.Any()) + { + return new PagedResponse(Enumerable.Empty(), query.PageNumber, query.PageSize, 0); + } + + // Paginate the views + var pagedViews = userViews.Views + .OrderByDescending(v => v.Date) + .Skip((query.PageNumber - 1) * query.PageSize) + .Take(query.PageSize) + .Select(v => new ViewDto { PostId = v.PostId, Date = v.Date }) + .ToList(); + + return new PagedResponse(pagedViews, query.PageNumber, query.PageSize, userViews.Views.Count()); + } + } +} diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Repositories/PostsUserViewsMongoRepository.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Repositories/PostsUserViewsMongoRepository.cs new file mode 100644 index 000000000..3a51102d1 --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Repositories/PostsUserViewsMongoRepository.cs @@ -0,0 +1,43 @@ +using System; +using System.Threading.Tasks; +using MongoDB.Driver; +using Convey.Persistence.MongoDB; +using MiniSpace.Services.Posts.Core.Repositories; +using MiniSpace.Services.Posts.Infrastructure.Mongo.Documents; +using MiniSpace.Services.Posts.Core.Entities; + +namespace MiniSpace.Services.Events.Infrastructure.Mongo.Repositories +{ + public class PostsUserViewsMongoRepository : IPostsUserViewsRepository + { + private readonly IMongoRepository _repository; + + public PostsUserViewsMongoRepository(IMongoRepository repository) + { + _repository = repository; + } + + public async Task GetAsync(Guid userId) + { + var document = await _repository.GetAsync(x => x.UserId == userId); + return document?.ToEntity(); + } + + public async Task AddAsync(PostsViews postsViews) + { + var document = postsViews.AsDocument(); + await _repository.AddAsync(document); + } + + public async Task UpdateAsync(PostsViews postsViews) + { + var document = postsViews.AsDocument(); + await _repository.UpdateAsync(document); + } + + public async Task DeleteAsync(Guid userId) + { + await _repository.DeleteAsync(x => x.UserId == userId); + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Api/Program.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Api/Program.cs index 5e55a750a..97903c2cb 100644 --- a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Api/Program.cs +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Api/Program.cs @@ -14,6 +14,7 @@ using MiniSpace.Services.Students.Application.Commands; using MiniSpace.Services.Students.Application.Dto; using MiniSpace.Services.Students.Application.Queries; +using MiniSpace.Services.Students.Core.Wrappers; using MiniSpace.Services.Students.Infrastructure; namespace MiniSpace.Services.Students.Api @@ -39,7 +40,11 @@ public static async Task Main(string[] args) .Get("students/{studentId}/visibility-settings") .Get("students/{studentId}/events") .Get("students/{studentId}/notifications") - + .Get>("students/profiles/users/{userId}/views/paginated") + .Get>("students/profiles/users/{userId}/views/viewed") + .Get>("students/{blockerId}/blocked-users") + + .Put("students/{studentId}") .Put("students/{studentId}/settings") .Put("students/{studentId}/state/{state}", @@ -47,11 +52,16 @@ public static async Task Main(string[] args) .Put("students/{studentId}/languages-and-interests") .Delete("students/{studentId}") - + + .Post("students/{blockerId}/block-user/{blockedUserId}", + afterDispatch: (cmd, ctx) => ctx.Response.Ok()) + .Post("students/{blockerId}/unblock-user/{blockedUserId}", + afterDispatch: (cmd, ctx) => ctx.Response.Ok()) + .Post("students", afterDispatch: (cmd, ctx) => ctx.Response.Created($"students/{cmd.StudentId}")) .Post("students/{studentId}/notifications") - + .Post("students/profiles/users/{userProfileId}/view", afterDispatch: (cmd, ctx) => ctx.Response.Ok()) )) .UseLogging() .Build() diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Commands/BlockUser.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Commands/BlockUser.cs new file mode 100644 index 000000000..c5641193f --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Commands/BlockUser.cs @@ -0,0 +1,17 @@ +using Convey.CQRS.Commands; +using System; + +namespace MiniSpace.Services.Students.Application.Commands +{ + public class BlockUser : ICommand + { + public Guid BlockerId { get; } + public Guid BlockedUserId { get; } + + public BlockUser(Guid blockerId, Guid blockedUserId) + { + BlockerId = blockerId; + BlockedUserId = blockedUserId; + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Commands/Handlers/BlockUserHandler.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Commands/Handlers/BlockUserHandler.cs new file mode 100644 index 000000000..6a4b53bf7 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Commands/Handlers/BlockUserHandler.cs @@ -0,0 +1,62 @@ +using Convey.CQRS.Commands; +using MiniSpace.Services.Students.Application.Exceptions; +using MiniSpace.Services.Students.Application.Services; +using MiniSpace.Services.Students.Core.Entities; +using MiniSpace.Services.Students.Core.Repositories; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace MiniSpace.Services.Students.Application.Commands.Handlers +{ + public class BlockUserHandler : ICommandHandler + { + private readonly IBlockedUsersRepository _blockedUsersRepository; + private readonly IStudentRepository _studentRepository; + private readonly IEventMapper _eventMapper; + private readonly IMessageBroker _messageBroker; + + public BlockUserHandler(IBlockedUsersRepository blockedUsersRepository, IStudentRepository studentRepository, IEventMapper eventMapper, IMessageBroker messageBroker) + { + _blockedUsersRepository = blockedUsersRepository; + _studentRepository = studentRepository; + _eventMapper = eventMapper; + _messageBroker = messageBroker; + } + + public async Task HandleAsync(BlockUser command, CancellationToken cancellationToken = default) + { + // Ensure the blocker exists + var blocker = await _studentRepository.GetAsync(command.BlockerId); + if (blocker is null) + { + throw new StudentNotFoundException(command.BlockerId); + } + + // Ensure the user to be blocked exists + var blockedUser = await _studentRepository.GetAsync(command.BlockedUserId); + if (blockedUser is null) + { + throw new StudentNotFoundException(command.BlockedUserId); + } + + // Fetch or create the BlockedUsers aggregate + var blockedUsersAggregate = await _blockedUsersRepository.GetAsync(command.BlockerId); + if (blockedUsersAggregate == null) + { + blockedUsersAggregate = new BlockedUsers(command.BlockerId); + await _blockedUsersRepository.AddAsync(blockedUsersAggregate); + } + + // Block the user + blockedUsersAggregate.BlockUser(command.BlockedUserId); + + // Update the repository to save changes + await _blockedUsersRepository.UpdateAsync(blockedUsersAggregate); + + // Publish domain events + var events = _eventMapper.MapAll(blockedUsersAggregate.Events); + await _messageBroker.PublishAsync(events.ToArray()); + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Commands/Handlers/UnblockUserHandler.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Commands/Handlers/UnblockUserHandler.cs new file mode 100644 index 000000000..75ae90a07 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Commands/Handlers/UnblockUserHandler.cs @@ -0,0 +1,65 @@ +using Convey.CQRS.Commands; +using MiniSpace.Services.Students.Application.Exceptions; +using MiniSpace.Services.Students.Application.Services; +using MiniSpace.Services.Students.Core.Entities; +using MiniSpace.Services.Students.Core.Repositories; +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace MiniSpace.Services.Students.Application.Commands.Handlers +{ + public class UnblockUserHandler : ICommandHandler + { + private readonly IBlockedUsersRepository _blockedUsersRepository; + private readonly IStudentRepository _studentRepository; + private readonly IEventMapper _eventMapper; + private readonly IMessageBroker _messageBroker; + + public UnblockUserHandler(IBlockedUsersRepository blockedUsersRepository, IStudentRepository studentRepository, IEventMapper eventMapper, IMessageBroker messageBroker) + { + _blockedUsersRepository = blockedUsersRepository; + _studentRepository = studentRepository; + _eventMapper = eventMapper; + _messageBroker = messageBroker; + } + + public async Task HandleAsync(UnblockUser command, CancellationToken cancellationToken = default) + { + var blocker = await _studentRepository.GetAsync(command.BlockerId); + var blockedUser = await _studentRepository.GetAsync(command.BlockedUserId); + + if (blocker is null) + { + throw new StudentNotFoundException(command.BlockerId); + } + + if (blockedUser is null) + { + throw new StudentNotFoundException(command.BlockedUserId); + } + + var blockedUsersAggregate = await _blockedUsersRepository.GetAsync(command.BlockerId); + + if (blockedUsersAggregate == null || !blockedUsersAggregate.BlockedUsersList.Any(b => b.BlockedUserId == command.BlockedUserId)) + { + throw new UserNotBlockedException(command.BlockerId, command.BlockedUserId); + } + + blockedUsersAggregate.UnblockUser(command.BlockedUserId); + + if (!blockedUsersAggregate.BlockedUsersList.Any()) + { + await _blockedUsersRepository.DeleteAsync(command.BlockerId); + } + else + { + await _blockedUsersRepository.UpdateAsync(blockedUsersAggregate); + } + + var events = _eventMapper.MapAll(blockedUsersAggregate.Events); + await _messageBroker.PublishAsync(events.ToArray()); + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Commands/Handlers/ViewUserProfileHandler.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Commands/Handlers/ViewUserProfileHandler.cs new file mode 100644 index 000000000..ea4330070 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Commands/Handlers/ViewUserProfileHandler.cs @@ -0,0 +1,65 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Convey.CQRS.Commands; +using MiniSpace.Services.Students.Core.Entities; +using MiniSpace.Services.Students.Core.Repositories; +using Microsoft.Extensions.Logging; +using Microsoft.AspNetCore.Http; +using MiniSpace.Services.Students.Application.Commands; +using MiniSpace.Services.Students.Application.Services; + +namespace MiniSpace.Services.Students.Application.Commands.Handlers +{ + public class ViewUserProfileHandler : ICommandHandler + { + private readonly IUserProfileViewsForUserRepository _userProfileViewsForUserRepository; + private readonly IUserViewingProfilesRepository _userViewingProfilesRepository; + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly IDeviceInfoService _deviceInfoService; + private readonly ILogger _logger; + + public ViewUserProfileHandler( + IUserProfileViewsForUserRepository userProfileViewsForUserRepository, + IUserViewingProfilesRepository userViewingProfilesRepository, + IHttpContextAccessor httpContextAccessor, + IDeviceInfoService deviceInfoService, + ILogger logger) + { + _userProfileViewsForUserRepository = userProfileViewsForUserRepository; + _userViewingProfilesRepository = userViewingProfilesRepository; + _httpContextAccessor = httpContextAccessor; + _deviceInfoService = deviceInfoService; + _logger = logger; + } + + public async Task HandleAsync(ViewUserProfile command, CancellationToken cancellationToken) + { + var httpContext = _httpContextAccessor.HttpContext; + var deviceInfo = _deviceInfoService.GetDeviceInfo(httpContext); + + // Handle views of the user profile being viewed + var userViewsForUser = await _userProfileViewsForUserRepository.GetAsync(command.UserProfileId); + if (userViewsForUser == null) + { + userViewsForUser = new UserProfileViewsForUser(command.UserProfileId, Enumerable.Empty()); + } + + userViewsForUser.AddView(command.UserId, DateTime.UtcNow, deviceInfo.IpAddress, deviceInfo.DeviceType, deviceInfo.OperatingSystem); + await _userProfileViewsForUserRepository.UpdateAsync(userViewsForUser); + + // Handle views by the user viewing profiles + var userViewingProfiles = await _userViewingProfilesRepository.GetAsync(command.UserId); + if (userViewingProfiles == null) + { + userViewingProfiles = new UserViewingProfiles(command.UserId, Enumerable.Empty()); + } + + userViewingProfiles.AddViewedProfile(command.UserProfileId, DateTime.UtcNow, deviceInfo.IpAddress, deviceInfo.DeviceType, deviceInfo.OperatingSystem); + await _userViewingProfilesRepository.UpdateAsync(userViewingProfiles); + + _logger.LogInformation($"User {command.UserId} viewed user profile {command.UserProfileId} from IP {deviceInfo.IpAddress} using {deviceInfo.DeviceType} ({deviceInfo.OperatingSystem})."); + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Commands/UnblockUser.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Commands/UnblockUser.cs new file mode 100644 index 000000000..f34dc7f3b --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Commands/UnblockUser.cs @@ -0,0 +1,17 @@ +using Convey.CQRS.Commands; +using System; + +namespace MiniSpace.Services.Students.Application.Commands +{ + public class UnblockUser : ICommand + { + public Guid BlockerId { get; } + public Guid BlockedUserId { get; } + + public UnblockUser(Guid blockerId, Guid blockedUserId) + { + BlockerId = blockerId; + BlockedUserId = blockedUserId; + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Commands/ViewUserProfile.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Commands/ViewUserProfile.cs new file mode 100644 index 000000000..9f3c9c8a3 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Commands/ViewUserProfile.cs @@ -0,0 +1,17 @@ +using System; +using Convey.CQRS.Commands; + +namespace MiniSpace.Services.Students.Application.Commands +{ + public class ViewUserProfile : ICommand + { + public Guid UserId { get; } + public Guid UserProfileId { get; } + + public ViewUserProfile(Guid userId, Guid userProfileId) + { + UserId = userId; + UserProfileId = userProfileId; + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Dto/BlockedUserDto.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Dto/BlockedUserDto.cs new file mode 100644 index 000000000..923ca97e4 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Dto/BlockedUserDto.cs @@ -0,0 +1,9 @@ +namespace MiniSpace.Services.Students.Application.Dto +{ + public class BlockedUserDto + { + public Guid BlockerId { get; set; } + public Guid BlockedUserId { get; set; } + public DateTime BlockedAt { get; set; } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Dto/UserProfileViewDto.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Dto/UserProfileViewDto.cs new file mode 100644 index 000000000..b9639c9fa --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Dto/UserProfileViewDto.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace MiniSpace.Services.Students.Application.Dto +{ + public class UserProfileViewDto + { + public Guid UserProfileId { get; set; } + public DateTime Date { get; set; } + public string IpAddress { get; set; } + public string DeviceType { get; set; } + public string OperatingSystem { get; set; } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/UserBlocked.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/UserBlocked.cs new file mode 100644 index 000000000..120e49050 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/UserBlocked.cs @@ -0,0 +1,17 @@ +using Convey.CQRS.Events; +using System; + +namespace MiniSpace.Services.Students.Application.Events +{ + public class UserBlocked : IEvent + { + public Guid BlockerId { get; } + public Guid BlockedUserId { get; } + + public UserBlocked(Guid blockerId, Guid blockedUserId) + { + BlockerId = blockerId; + BlockedUserId = blockedUserId; + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Exceptions/UserAlreadyBlockedException.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Exceptions/UserAlreadyBlockedException.cs new file mode 100644 index 000000000..41bd5abd9 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Exceptions/UserAlreadyBlockedException.cs @@ -0,0 +1,12 @@ +namespace MiniSpace.Services.Students.Application.Exceptions +{ + public class UserAlreadyBlockedException : AppException + { + public override string Code { get; } = "user_already_blocked"; + + public UserAlreadyBlockedException(Guid blockerId, Guid blockedUserId) + : base($"User with ID: '{blockedUserId}' is already blocked by user with ID: '{blockerId}'.") + { + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Exceptions/UserNotBlockedException.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Exceptions/UserNotBlockedException.cs new file mode 100644 index 000000000..0d2baced3 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Exceptions/UserNotBlockedException.cs @@ -0,0 +1,14 @@ +using System; + +namespace MiniSpace.Services.Students.Application.Exceptions +{ + public class UserNotBlockedException : AppException + { + public override string Code { get; } = "user_not_blocked"; + + public UserNotBlockedException(Guid blockerId, Guid blockedUserId) + : base($"User with ID '{blockedUserId}' is not blocked by user with ID '{blockerId}'.") + { + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Queries/GetBlockedUsers.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Queries/GetBlockedUsers.cs new file mode 100644 index 000000000..95ef7c7d9 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Queries/GetBlockedUsers.cs @@ -0,0 +1,26 @@ +using Convey.CQRS.Queries; +using MiniSpace.Services.Students.Application.Dto; +using MiniSpace.Services.Students.Core.Wrappers; +using System.Diagnostics.CodeAnalysis; + +namespace MiniSpace.Services.Students.Application.Queries +{ + [ExcludeFromCodeCoverage] + public class GetBlockedUsers : IQuery> + { + public Guid BlockerId { get; set; } + public int Page { get; set; } + public int ResultsPerPage { get; set; } + public string OrderBy { get; set; } + public string SortOrder { get; set; } + + public GetBlockedUsers(Guid blockerId, int page, int resultsPerPage, string orderBy = "BlockedAt", string sortOrder = "desc") + { + BlockerId = blockerId; + Page = page; + ResultsPerPage = resultsPerPage; + OrderBy = orderBy; + SortOrder = sortOrder; + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Queries/GetProfilesViewedByUser.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Queries/GetProfilesViewedByUser.cs new file mode 100644 index 000000000..9602600d4 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Queries/GetProfilesViewedByUser.cs @@ -0,0 +1,21 @@ +using System; +using Convey.CQRS.Queries; +using MiniSpace.Services.Students.Application.Dto; +using MiniSpace.Services.Students.Core.Wrappers; + +namespace MiniSpace.Services.Students.Application.Queries +{ + public class GetProfilesViewedByUser : IQuery> + { + public Guid UserId { get; } + public int PageNumber { get; } + public int PageSize { get; } + + public GetProfilesViewedByUser(Guid userId, int pageNumber = 1, int pageSize = 10) + { + UserId = userId; + PageNumber = pageNumber; + PageSize = pageSize; + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Queries/GetUserProfileViews.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Queries/GetUserProfileViews.cs new file mode 100644 index 000000000..dd8344685 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Queries/GetUserProfileViews.cs @@ -0,0 +1,21 @@ +using System; +using Convey.CQRS.Queries; +using MiniSpace.Services.Students.Application.Dto; +using MiniSpace.Services.Students.Core.Wrappers; + +namespace MiniSpace.Services.Students.Application.Queries +{ + public class GetUserProfileViews : IQuery> + { + public Guid UserId { get; } + public int PageNumber { get; } + public int PageSize { get; } + + public GetUserProfileViews(Guid userId, int pageNumber = 1, int pageSize = 10) + { + UserId = userId; + PageNumber = pageNumber; + PageSize = pageSize; + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Services/IDeviceInfoService.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Services/IDeviceInfoService.cs new file mode 100644 index 000000000..f7272f6d7 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Services/IDeviceInfoService.cs @@ -0,0 +1,11 @@ +using Microsoft.AspNetCore.Http; +using MiniSpace.Services.Students.Core.Entities; +using System; + +namespace MiniSpace.Services.Students.Application.Services +{ + public interface IDeviceInfoService + { + DeviceInfo GetDeviceInfo(HttpContext httpContext); + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Entities/BlockedUser.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Entities/BlockedUser.cs new file mode 100644 index 000000000..d62c754f6 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Entities/BlockedUser.cs @@ -0,0 +1,18 @@ +using System; + +namespace MiniSpace.Services.Students.Core.Entities +{ + public class BlockedUser + { + public Guid BlockerId { get; private set; } + public Guid BlockedUserId { get; private set; } + public DateTime BlockedAt { get; private set; } + + public BlockedUser(Guid blockerId, Guid blockedUserId, DateTime blockedAt) + { + BlockerId = blockerId; + BlockedUserId = blockedUserId; + BlockedAt = blockedAt; + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Entities/BlockedUsers.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Entities/BlockedUsers.cs new file mode 100644 index 000000000..fa23caf66 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Entities/BlockedUsers.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using MiniSpace.Services.Students.Core.Events; + +namespace MiniSpace.Services.Students.Core.Entities +{ + public class BlockedUsers : AggregateRoot + { + private readonly ISet _blockedUsers = new HashSet(); + + public BlockedUsers(Guid userId) + { + Id = userId; + UserId = userId; + } + + public Guid UserId { get; private set; } + public IEnumerable BlockedUsersList => _blockedUsers; + + public void BlockUser(Guid blockedUserId) + { + if (blockedUserId == Guid.Empty || _blockedUsers.Any(b => b.BlockedUserId == blockedUserId)) + { + throw new InvalidOperationException($"User with ID {blockedUserId} cannot be blocked or is already blocked."); + } + + var blockedUser = new BlockedUser(UserId, blockedUserId, DateTime.UtcNow); + _blockedUsers.Add(blockedUser); + AddEvent(new UserBlockedEvent(this, blockedUserId)); + } + + public void UnblockUser(Guid blockedUserId) + { + var blockedUser = _blockedUsers.SingleOrDefault(b => b.BlockedUserId == blockedUserId); + if (blockedUser == null) + { + throw new InvalidOperationException($"User with ID {blockedUserId} is not blocked."); + } + + _blockedUsers.Remove(blockedUser); + AddEvent(new UserUnblockedEvent(this, blockedUserId)); + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Entities/DeviceInfo.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Entities/DeviceInfo.cs new file mode 100644 index 000000000..19cc88e00 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Entities/DeviceInfo.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace MiniSpace.Services.Students.Core.Entities +{ + public class DeviceInfo + { + public string IpAddress { get; set; } + public string DeviceType { get; set; } + public string OperatingSystem { get; set; } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Entities/Student.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Entities/Student.cs index 0bbb02781..5a6cfde4c 100644 --- a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Entities/Student.cs +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Entities/Student.cs @@ -14,7 +14,6 @@ public class Student : AggregateRoot private ISet _interests = new HashSet(); private ISet _education = new HashSet(); private ISet _work = new HashSet(); - public string Email { get; private set; } public string FirstName { get; private set; } public string LastName { get; private set; } diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Entities/UserProfileView.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Entities/UserProfileView.cs new file mode 100644 index 000000000..0970c8eff --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Entities/UserProfileView.cs @@ -0,0 +1,22 @@ +using System; + +namespace MiniSpace.Services.Students.Core.Entities +{ + public class UserProfileView + { + public Guid UserProfileId { get; private set; } + public DateTime Date { get; private set; } + public string IpAddress { get; private set; } + public string DeviceType { get; private set; } + public string OperatingSystem { get; private set; } + + public UserProfileView(Guid userProfileId, DateTime date, string ipAddress, string deviceType, string operatingSystem) + { + UserProfileId = userProfileId; + Date = date; + IpAddress = ipAddress; + DeviceType = deviceType; + OperatingSystem = operatingSystem; + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Entities/UserProfileViewsForUser.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Entities/UserProfileViewsForUser.cs new file mode 100644 index 000000000..1b9655e09 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Entities/UserProfileViewsForUser.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace MiniSpace.Services.Students.Core.Entities +{ + public class UserProfileViewsForUser + { + public Guid UserId { get; private set; } + public IEnumerable Views { get; private set; } + + public UserProfileViewsForUser(Guid userId, IEnumerable views) + { + UserId = userId; + Views = views ?? new List(); + } + + public void AddView(Guid userProfileId, DateTime date, string ipAddress, string deviceType, string operatingSystem) + { + var viewList = new List(Views) + { + new UserProfileView(userProfileId, date, ipAddress, deviceType, operatingSystem) + }; + Views = viewList; + } + + public void RemoveView(Guid userProfileId) + { + var viewList = new List(Views); + var viewToRemove = viewList.Find(view => view.UserProfileId == userProfileId); + if (viewToRemove != null) + { + viewList.Remove(viewToRemove); + Views = viewList; + } + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Entities/UserViewingProfiles.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Entities/UserViewingProfiles.cs new file mode 100644 index 000000000..b9b536809 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Entities/UserViewingProfiles.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace MiniSpace.Services.Students.Core.Entities +{ + public class UserViewingProfiles + { + public Guid UserId { get; private set; } + public IEnumerable ViewedProfiles { get; private set; } + + public UserViewingProfiles(Guid userId, IEnumerable viewedProfiles) + { + UserId = userId; + ViewedProfiles = viewedProfiles ?? new List(); + } + + public void AddViewedProfile(Guid userProfileId, DateTime date, string ipAddress, string deviceType, string operatingSystem) + { + var viewedList = new List(ViewedProfiles) + { + new UserProfileView(userProfileId, date, ipAddress, deviceType, operatingSystem) + }; + ViewedProfiles = viewedList; + } + + public void RemoveViewedProfile(Guid userProfileId) + { + var viewedList = new List(ViewedProfiles); + var profileToRemove = viewedList.Find(view => view.UserProfileId == userProfileId); + if (profileToRemove != null) + { + viewedList.Remove(profileToRemove); + ViewedProfiles = viewedList; + } + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Events/UserBlockedEvent.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Events/UserBlockedEvent.cs new file mode 100644 index 000000000..0b3f0d8d4 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Events/UserBlockedEvent.cs @@ -0,0 +1,16 @@ +using MiniSpace.Services.Students.Core.Entities; + +namespace MiniSpace.Services.Students.Core.Events +{ + public class UserBlockedEvent : IDomainEvent + { + public BlockedUsers BlockedUsers { get; } + public Guid BlockedUserId { get; } + + public UserBlockedEvent(BlockedUsers blockedUsers, Guid blockedUserId) + { + BlockedUsers = blockedUsers; + BlockedUserId = blockedUserId; + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Events/UserUnblockedEvent.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Events/UserUnblockedEvent.cs new file mode 100644 index 000000000..e7dab95c8 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Events/UserUnblockedEvent.cs @@ -0,0 +1,16 @@ +using MiniSpace.Services.Students.Core.Entities; + +namespace MiniSpace.Services.Students.Core.Events +{ + public class UserUnblockedEvent : IDomainEvent + { + public BlockedUsers BlockedUsers { get; } + public Guid UnblockedUserId { get; } + + public UserUnblockedEvent(BlockedUsers blockedUsers, Guid unblockedUserId) + { + BlockedUsers = blockedUsers; + UnblockedUserId = unblockedUserId; + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Repositories/IBlockedUsersRepository.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Repositories/IBlockedUsersRepository.cs new file mode 100644 index 000000000..5c541bffc --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Repositories/IBlockedUsersRepository.cs @@ -0,0 +1,14 @@ +using MiniSpace.Services.Students.Core.Entities; +using System; +using System.Threading.Tasks; + +namespace MiniSpace.Services.Students.Core.Repositories +{ + public interface IBlockedUsersRepository + { + Task GetAsync(Guid userId); + Task AddAsync(BlockedUsers blockedUsers); + Task UpdateAsync(BlockedUsers blockedUsers); + Task DeleteAsync(Guid userId); + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Repositories/IUserProfileViewsForUserRepository.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Repositories/IUserProfileViewsForUserRepository.cs new file mode 100644 index 000000000..a2bbe3c41 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Repositories/IUserProfileViewsForUserRepository.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using MiniSpace.Services.Students.Core.Entities; + +namespace MiniSpace.Services.Students.Core.Repositories +{ + public interface IUserProfileViewsForUserRepository + { + Task GetAsync(Guid userId); + Task AddAsync(UserProfileViewsForUser userProfileViews); + Task UpdateAsync(UserProfileViewsForUser userProfileViews); + Task DeleteAsync(Guid userId); + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Repositories/IUserViewingProfilesRepository.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Repositories/IUserViewingProfilesRepository.cs new file mode 100644 index 000000000..050fb7ee2 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Repositories/IUserViewingProfilesRepository.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using MiniSpace.Services.Students.Core.Entities; + +namespace MiniSpace.Services.Students.Core.Repositories +{ + public interface IUserViewingProfilesRepository + { + Task GetAsync(Guid userId); + Task AddAsync(UserViewingProfiles userViewingProfiles); + Task UpdateAsync(UserViewingProfiles userViewingProfiles); + Task DeleteAsync(Guid userId); + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Wrappers/PagedResponse.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Wrappers/PagedResponse.cs new file mode 100644 index 000000000..95197acb6 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Wrappers/PagedResponse.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace MiniSpace.Services.Students.Core.Wrappers +{ + public class PagedResponse + { + public IEnumerable Items { get; } + public int TotalPages { get; } + public int TotalItems { get; } + public int PageSize { get; } + public int Page { get; } + public bool First { get; } + public bool Last { get; } + public bool Empty { get; } + public int? NextPage => Page < TotalPages ? Page + 1 : (int?)null; + public int? PreviousPage => Page > 1 ? Page - 1 : (int?)null; + + public PagedResponse(IEnumerable items, int page, int pageSize, int totalItems) + { + Items = items; + PageSize = pageSize; + TotalItems = totalItems; + TotalPages = pageSize > 0 ? (int)Math.Ceiling((decimal)totalItems / pageSize) : 0; + Page = page; + First = page == 1; + Last = page == TotalPages; + Empty = !items.Any(); + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Extensions.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Extensions.cs index d0e50d304..fb3b66923 100644 --- a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Extensions.cs +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Extensions.cs @@ -54,9 +54,14 @@ public static IConveyBuilder AddInfrastructure(this IConveyBuilder builder) builder.Services.AddTransient(); builder.Services.AddTransient(); builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddTransient(); + builder.Services.AddTransient(); builder.Services.AddTransient(); builder.Services.AddTransient(ctx => ctx.GetRequiredService().Create()); builder.Services.TryDecorate(typeof(ICommandHandler<>), typeof(OutboxCommandHandlerDecorator<>)); @@ -81,6 +86,9 @@ public static IConveyBuilder AddInfrastructure(this IConveyBuilder builder) .AddMongoRepository("user-notifications") .AddMongoRepository("user-settings") .AddMongoRepository("user-gellery") + .AddMongoRepository("user_profile_views") + .AddMongoRepository("user_viewing_profiles") + .AddMongoRepository("blocked_users") .AddWebApiSwaggerDocs() .AddCertificateAuthentication() .AddSecurity(); diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Documents/BlockedUsersDocument.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Documents/BlockedUsersDocument.cs new file mode 100644 index 000000000..a1b568ede --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Documents/BlockedUsersDocument.cs @@ -0,0 +1,21 @@ +using Convey.Types; +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace MiniSpace.Services.Students.Infrastructure.Mongo.Documents +{ + [ExcludeFromCodeCoverage] + public class BlockedUsersDocument : IIdentifiable + { + public Guid Id { get; set; } + public Guid UserId { get; set; } + public IEnumerable BlockedUsers { get; set; } = new List(); + + public class BlockedUserEntry + { + public Guid BlockedUserId { get; set; } + public DateTime BlockedAt { get; set; } + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Documents/BlockedUsersExtensions.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Documents/BlockedUsersExtensions.cs new file mode 100644 index 000000000..0953a9a27 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Documents/BlockedUsersExtensions.cs @@ -0,0 +1,52 @@ +using MiniSpace.Services.Students.Core.Entities; +using MiniSpace.Services.Students.Infrastructure.Mongo.Documents; +using MiniSpace.Services.Students.Application.Dto; +using System.Linq; +using System.Collections.Generic; + +namespace MiniSpace.Services.Students.Infrastructure.Mongo.Documents +{ + public static class BlockedUsersExtensions + { + public static BlockedUsersDocument AsDocument(this BlockedUsers blockedUsers) + { + return new BlockedUsersDocument + { + Id = blockedUsers.UserId, + UserId = blockedUsers.UserId, + BlockedUsers = blockedUsers.BlockedUsersList.Select(b => new BlockedUsersDocument.BlockedUserEntry + { + BlockedUserId = b.BlockedUserId, + BlockedAt = b.BlockedAt + }).ToList() + }; + } + + public static BlockedUsers AsEntity(this BlockedUsersDocument document) + { + var blockedUsers = new BlockedUsers(document.UserId); + foreach (var entry in document.BlockedUsers) + { + var blockedUser = new BlockedUser(document.UserId, entry.BlockedUserId, entry.BlockedAt); + blockedUsers.BlockUser(blockedUser.BlockedUserId); + } + + return blockedUsers; + } + + public static BlockedUserDto AsDto(this BlockedUsersDocument.BlockedUserEntry blockedUserEntry, Guid blockerId) + { + return new BlockedUserDto + { + BlockerId = blockerId, + BlockedUserId = blockedUserEntry.BlockedUserId, + BlockedAt = blockedUserEntry.BlockedAt + }; + } + + public static IEnumerable AsDto(this BlockedUsersDocument document) + { + return document.BlockedUsers.Select(b => b.AsDto(document.UserId)); + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Documents/UserProfileViewDocument.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Documents/UserProfileViewDocument.cs new file mode 100644 index 000000000..4ef955c15 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Documents/UserProfileViewDocument.cs @@ -0,0 +1,34 @@ +using System; +using Convey.Types; +using MiniSpace.Services.Students.Core.Entities; + +namespace MiniSpace.Services.Students.Infrastructure.Mongo.Documents +{ + public class UserProfileViewDocument : IIdentifiable + { + public Guid Id { get; set; } + public Guid UserProfileId { get; set; } + public DateTime Date { get; set; } + public string IpAddress { get; set; } + public string DeviceType { get; set; } + public string OperatingSystem { get; set; } + + public static UserProfileViewDocument FromEntity(UserProfileView view) + { + return new UserProfileViewDocument + { + Id = Guid.NewGuid(), + UserProfileId = view.UserProfileId, + Date = view.Date, + IpAddress = view.IpAddress, + DeviceType = view.DeviceType, + OperatingSystem = view.OperatingSystem + }; + } + + public UserProfileView ToEntity() + { + return new UserProfileView(UserProfileId, Date, IpAddress, DeviceType, OperatingSystem); + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Documents/UserProfileViewsDocument.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Documents/UserProfileViewsDocument.cs new file mode 100644 index 000000000..8050d1571 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Documents/UserProfileViewsDocument.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Convey.Types; +using MiniSpace.Services.Students.Core.Entities; + +namespace MiniSpace.Services.Students.Infrastructure.Mongo.Documents +{ + public class UserProfileViewsDocument : IIdentifiable + { + public Guid Id { get; set; } + public Guid UserId { get; set; } + public List Views { get; set; } = new List(); + + public static UserProfileViewsDocument FromEntity(UserProfileViewsForUser userProfileViews) + { + return new UserProfileViewsDocument + { + Id = Guid.NewGuid(), + UserId = userProfileViews.UserId, + Views = userProfileViews.Views.Select(UserProfileViewDocument.FromEntity).ToList() + }; + } + + public UserProfileViewsForUser ToEntity() + { + return new UserProfileViewsForUser(UserId, Views.Select(view => view.ToEntity())); + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Documents/UserProfileViewsExtensions.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Documents/UserProfileViewsExtensions.cs new file mode 100644 index 000000000..1fb2795ff --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Documents/UserProfileViewsExtensions.cs @@ -0,0 +1,39 @@ + +using System.Linq; +using MiniSpace.Services.Students.Core.Entities; + +namespace MiniSpace.Services.Students.Infrastructure.Mongo.Documents +{ + public static class UserProfileViewsExtensions + { + public static UserProfileViewsDocument AsDocument(this UserProfileViewsForUser entity) + { + return UserProfileViewsDocument.FromEntity(entity); + } + + public static UserProfileViewsForUser AsEntity(this UserProfileViewsDocument document) + { + return document.ToEntity(); + } + + public static UserProfileViewDocument AsDocument(this UserProfileView view) + { + return UserProfileViewDocument.FromEntity(view); + } + + public static UserProfileView AsEntity(this UserProfileViewDocument document) + { + return document.ToEntity(); + } + + public static UserViewingProfilesDocument AsDocument(this UserViewingProfiles entity) + { + return UserViewingProfilesDocument.FromEntity(entity); + } + + public static UserViewingProfiles AsEntity(this UserViewingProfilesDocument document) + { + return document.ToEntity(); + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Documents/UserViewingProfilesDocument.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Documents/UserViewingProfilesDocument.cs new file mode 100644 index 000000000..fde8d2e2c --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Documents/UserViewingProfilesDocument.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Convey.Types; +using MiniSpace.Services.Students.Core.Entities; + +namespace MiniSpace.Services.Students.Infrastructure.Mongo.Documents +{ + public class UserViewingProfilesDocument : IIdentifiable + { + public Guid Id { get; set; } + public Guid UserId { get; set; } + public List ViewedProfiles { get; set; } = new List(); + + public static UserViewingProfilesDocument FromEntity(UserViewingProfiles userViewingProfiles) + { + return new UserViewingProfilesDocument + { + Id = Guid.NewGuid(), + UserId = userViewingProfiles.UserId, + ViewedProfiles = userViewingProfiles.ViewedProfiles.Select(UserProfileViewDocument.FromEntity).ToList() + }; + } + + public UserViewingProfiles ToEntity() + { + return new UserViewingProfiles(UserId, ViewedProfiles.Select(view => view.ToEntity())); + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Queries/Handlers/GetBlockedUsersHandler.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Queries/Handlers/GetBlockedUsersHandler.cs new file mode 100644 index 000000000..e02920025 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Queries/Handlers/GetBlockedUsersHandler.cs @@ -0,0 +1,59 @@ +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Convey.CQRS.Queries; +using MiniSpace.Services.Students.Application.Dto; +using MiniSpace.Services.Students.Application.Queries; +using MiniSpace.Services.Students.Core.Repositories; +using MiniSpace.Services.Students.Core.Wrappers; + +namespace MiniSpace.Services.Students.Application.Queries.Handlers +{ + public class GetBlockedUsersHandler : IQueryHandler> + { + private readonly IBlockedUsersRepository _blockedUsersRepository; + + public GetBlockedUsersHandler(IBlockedUsersRepository blockedUsersRepository) + { + _blockedUsersRepository = blockedUsersRepository; + } + + public async Task> HandleAsync(GetBlockedUsers query, CancellationToken cancellationToken = default) + { + var blockedUsersAggregate = await _blockedUsersRepository.GetAsync(query.BlockerId); + + if (blockedUsersAggregate == null || !blockedUsersAggregate.BlockedUsersList.Any()) + { + return new PagedResponse( + Enumerable.Empty(), + query.Page, + query.ResultsPerPage, + 0); + } + + var sortedBlockedUsers = query.SortOrder.ToLower() == "asc" + ? blockedUsersAggregate.BlockedUsersList.OrderBy(b => b.BlockedAt) + : blockedUsersAggregate.BlockedUsersList.OrderByDescending(b => b.BlockedAt); + + var totalItems = sortedBlockedUsers.Count(); + var totalPages = (int)System.Math.Ceiling(totalItems / (double)query.ResultsPerPage); + + var blockedUsersPage = sortedBlockedUsers + .Skip((query.Page - 1) * query.ResultsPerPage) + .Take(query.ResultsPerPage) + .Select(b => new BlockedUserDto + { + BlockerId = query.BlockerId, + BlockedUserId = b.BlockedUserId, + BlockedAt = b.BlockedAt + }) + .ToList(); + + return new PagedResponse( + blockedUsersPage, + query.Page, + query.ResultsPerPage, + totalItems); + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Queries/Handlers/GetProfilesViewedByUserHandler.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Queries/Handlers/GetProfilesViewedByUserHandler.cs new file mode 100644 index 000000000..12b537591 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Queries/Handlers/GetProfilesViewedByUserHandler.cs @@ -0,0 +1,49 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Convey.CQRS.Queries; +using MiniSpace.Services.Students.Application.Dto; +using MiniSpace.Services.Students.Core.Repositories; +using MiniSpace.Services.Students.Core.Wrappers; + +namespace MiniSpace.Services.Students.Application.Queries.Handlers +{ + public class GetProfilesViewedByUserHandler : IQueryHandler> + { + private readonly IUserViewingProfilesRepository _userViewingProfilesRepository; + + public GetProfilesViewedByUserHandler(IUserViewingProfilesRepository userViewingProfilesRepository) + { + _userViewingProfilesRepository = userViewingProfilesRepository; + } + + public async Task> HandleAsync(GetProfilesViewedByUser query, CancellationToken cancellationToken) + { + var userViewingProfiles = await _userViewingProfilesRepository.GetAsync(query.UserId); + + if (userViewingProfiles == null || !userViewingProfiles.ViewedProfiles.Any()) + { + return new PagedResponse(Enumerable.Empty(), query.PageNumber, query.PageSize, 0); + } + + var totalItems = userViewingProfiles.ViewedProfiles.Count(); + var totalPages = (int)Math.Ceiling(totalItems / (double)query.PageSize); + + var pagedViews = userViewingProfiles.ViewedProfiles + .Skip((query.PageNumber - 1) * query.PageSize) + .Take(query.PageSize) + .Select(view => new UserProfileViewDto + { + UserProfileId = view.UserProfileId, + Date = view.Date, + IpAddress = view.IpAddress, + DeviceType = view.DeviceType, + OperatingSystem = view.OperatingSystem + }) + .ToList(); + + return new PagedResponse(pagedViews, query.PageNumber, query.PageSize, totalItems); + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Queries/Handlers/GetUserProfileViewsHandler.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Queries/Handlers/GetUserProfileViewsHandler.cs new file mode 100644 index 000000000..89a28990e --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Queries/Handlers/GetUserProfileViewsHandler.cs @@ -0,0 +1,58 @@ +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Convey.CQRS.Queries; +using MiniSpace.Services.Students.Application.Dto; +using MiniSpace.Services.Students.Application.Queries; +using MiniSpace.Services.Students.Core.Repositories; +using MiniSpace.Services.Students.Core.Wrappers; + +namespace MiniSpace.Services.Students.Application.Queries.Handlers +{ + public class GetUserProfileViewsHandler : IQueryHandler> + { + private readonly IUserProfileViewsForUserRepository _userProfileViewsRepository; + + public GetUserProfileViewsHandler(IUserProfileViewsForUserRepository userProfileViewsRepository) + { + _userProfileViewsRepository = userProfileViewsRepository; + } + + public async Task> HandleAsync(GetUserProfileViews query, CancellationToken cancellationToken = default) + { + var userProfileViews = await _userProfileViewsRepository.GetAsync(query.UserId); + + if (userProfileViews == null || !userProfileViews.Views.Any()) + { + return new PagedResponse( + Enumerable.Empty(), + query.PageNumber, + query.PageSize, + 0); + } + + var totalItems = userProfileViews.Views.Count(); + var totalPages = (int)System.Math.Ceiling(totalItems / (double)query.PageSize); + + var views = userProfileViews.Views + .Skip((query.PageNumber - 1) * query.PageSize) + .Take(query.PageSize) + .Select(view => new UserProfileViewDto + { + UserProfileId = view.UserProfileId, + Date = view.Date, + IpAddress = view.IpAddress, + DeviceType = view.DeviceType, + OperatingSystem = view.OperatingSystem + }); + + var pagedResponse = new PagedResponse( + views, + query.PageNumber, + query.PageSize, + totalItems); + + return pagedResponse; + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Repositories/BlockedUsersMongoRepository.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Repositories/BlockedUsersMongoRepository.cs new file mode 100644 index 000000000..be16ef111 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Repositories/BlockedUsersMongoRepository.cs @@ -0,0 +1,54 @@ +using Convey.Persistence.MongoDB; +using MongoDB.Driver; +using MiniSpace.Services.Students.Core.Entities; +using MiniSpace.Services.Students.Core.Repositories; +using MiniSpace.Services.Students.Infrastructure.Mongo.Documents; +using System; +using System.Linq; +using System.Threading.Tasks; + +namespace MiniSpace.Services.Students.Infrastructure.Mongo.Repositories +{ + public class BlockedUsersMongoRepository : IBlockedUsersRepository + { + private readonly IMongoRepository _repository; + + public BlockedUsersMongoRepository(IMongoRepository repository) + { + _repository = repository; + } + + public async Task GetAsync(Guid blockerId) + { + var document = await _repository.GetAsync(d => d.UserId == blockerId); + return document?.AsEntity(); + } + + public async Task AddAsync(BlockedUsers blockedUsers) + { + var document = blockedUsers.AsDocument(); + await _repository.AddAsync(document); + } + + public async Task UpdateAsync(BlockedUsers blockedUsers) + { + var document = blockedUsers.AsDocument(); + var filter = Builders.Filter.Eq(d => d.UserId, document.UserId); + + // Replace the existing document + var result = await _repository.Collection.ReplaceOneAsync(filter, document, new ReplaceOptions { IsUpsert = true }); + + // Ensure the operation was successful + if (result.MatchedCount == 0 && result.UpsertedId == null) + { + throw new Exception("Failed to update the blocked user list."); + } + } + + public async Task DeleteAsync(Guid blockerId) + { + var filter = Builders.Filter.Eq(d => d.UserId, blockerId); + await _repository.Collection.DeleteOneAsync(filter); + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Repositories/UserProfileViewsRepository.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Repositories/UserProfileViewsRepository.cs new file mode 100644 index 000000000..f9d92fb43 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Repositories/UserProfileViewsRepository.cs @@ -0,0 +1,42 @@ +using System; +using System.Threading.Tasks; +using MiniSpace.Services.Students.Core.Entities; +using MiniSpace.Services.Students.Core.Repositories; +using MiniSpace.Services.Students.Infrastructure.Mongo.Documents; +using Convey.Persistence.MongoDB; + +namespace MiniSpace.Services.Students.Infrastructure.Mongo.Repositories +{ + public class UserProfileViewsRepository : IUserProfileViewsForUserRepository + { + private readonly IMongoRepository _repository; + + public UserProfileViewsRepository(IMongoRepository repository) + { + _repository = repository; + } + + public async Task GetAsync(Guid userId) + { + var document = await _repository.GetAsync(x => x.UserId == userId); + return document?.ToEntity(); + } + + public async Task AddAsync(UserProfileViewsForUser userProfileViews) + { + var document = userProfileViews.AsDocument(); + await _repository.AddAsync(document); + } + + public async Task UpdateAsync(UserProfileViewsForUser userProfileViews) + { + var document = userProfileViews.AsDocument(); + await _repository.UpdateAsync(document); + } + + public async Task DeleteAsync(Guid userId) + { + await _repository.DeleteAsync(x => x.UserId == userId); + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Repositories/UserViewingProfilesRepository.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Repositories/UserViewingProfilesRepository.cs new file mode 100644 index 000000000..e24526e2a --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Repositories/UserViewingProfilesRepository.cs @@ -0,0 +1,42 @@ +using System; +using System.Threading.Tasks; +using MiniSpace.Services.Students.Core.Entities; +using MiniSpace.Services.Students.Core.Repositories; +using MiniSpace.Services.Students.Infrastructure.Mongo.Documents; +using Convey.Persistence.MongoDB; + +namespace MiniSpace.Services.Students.Infrastructure.Mongo.Repositories +{ + public class UserViewingProfilesRepository : IUserViewingProfilesRepository + { + private readonly IMongoRepository _repository; + + public UserViewingProfilesRepository(IMongoRepository repository) + { + _repository = repository; + } + + public async Task GetAsync(Guid userId) + { + var document = await _repository.GetAsync(x => x.UserId == userId); + return document?.ToEntity(); + } + + public async Task AddAsync(UserViewingProfiles userViewingProfiles) + { + var document = userViewingProfiles.AsDocument(); + await _repository.AddAsync(document); + } + + public async Task UpdateAsync(UserViewingProfiles userViewingProfiles) + { + var document = userViewingProfiles.AsDocument(); + await _repository.UpdateAsync(document); + } + + public async Task DeleteAsync(Guid userId) + { + await _repository.DeleteAsync(x => x.UserId == userId); + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Options/MongoDbOptions.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Options/MongoDbOptions.cs deleted file mode 100644 index 029c915f2..000000000 --- a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Options/MongoDbOptions.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace MiniSpace.Services.Students.Infrastructure.Options -{ - public class MongoDbOptions - { - public string ConnectionString { get; set; } - public string WriteDatabase { get; set; } - public string ReadDatabase { get; set; } - public bool Seed { get; set; } - } -} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Services/DeviceInfoService.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Services/DeviceInfoService.cs new file mode 100644 index 000000000..bcaaa0eea --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Services/DeviceInfoService.cs @@ -0,0 +1,30 @@ +using Microsoft.AspNetCore.Http; +using MiniSpace.Services.Students.Application.Services; +using MiniSpace.Services.Students.Core.Entities; +using System; + +namespace MiniSpace.Services.Students.Infrastructure.Services +{ + public class DeviceInfoService : IDeviceInfoService + { + public DeviceInfo GetDeviceInfo(HttpContext httpContext) + { + var ipAddress = httpContext.Connection.RemoteIpAddress?.ToString() ?? "Unknown"; + + var userAgent = httpContext.Request.Headers["User-Agent"].ToString().ToLower(); + var deviceType = userAgent.Contains("mobile") ? "Mobile" : "Computer"; + var operatingSystem = userAgent.Contains("windows") ? "Windows" : + userAgent.Contains("mac") ? "MacOS" : + userAgent.Contains("android") ? "Android" : + userAgent.Contains("iphone") ? "iOS" : + userAgent.Contains("linux") ? "Linux" : "Unknown"; + + return new DeviceInfo + { + IpAddress = ipAddress, + DeviceType = deviceType, + OperatingSystem = operatingSystem + }; + } + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/App.razor b/MiniSpace.Web/src/MiniSpace.Web/App.razor index 079b7b5d4..fa3864aa3 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/App.razor +++ b/MiniSpace.Web/src/MiniSpace.Web/App.razor @@ -1,39 +1,7 @@ -@* @using Microsoft.AspNetCore.Components.Authorization -@using Radzen -@using MiniSpace.Web.Areas.Students -@inherits LayoutComponentBase -@inject IIdentityService IdentityService -@inject IStudentsService StudentsService -@inject NavigationManager NavigationManager -@inject Blazored.LocalStorage.ILocalStorageService localStorage -@inject CustomAuthenticationStateProvider CustomAuthenticationStateProvider - - - - - - - - - -

Sorry, there's nothing at this address.

-
-
-
-
- -@code { - protected override async Task OnInitializedAsync() - { - await CustomAuthenticationStateProvider.InitializeAsync(); - await base.OnInitializedAsync(); - } - -} *@ - - -@using Microsoft.AspNetCore.Components.Authorization +@using Microsoft.AspNetCore.Components.Authorization @using MudBlazor.Services +@using MudBlazor +@* *@ diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Communication/ChatSignalRService.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Communication/ChatSignalRService.cs new file mode 100644 index 000000000..56fcb33d8 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Communication/ChatSignalRService.cs @@ -0,0 +1,179 @@ +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.SignalR.Client; +using Microsoft.JSInterop; +using MiniSpace.Web.Areas.Identity; +using MiniSpace.Web.DTO.Communication; +using System; +using System.Threading.Tasks; + +namespace MiniSpace.Web.Areas.Communication +{ + public class ChatSignalRService : IAsyncDisposable + { + private HubConnection _hubConnection; + private readonly NavigationManager _navigationManager; + private readonly IIdentityService _identityService; + private Guid _userId; + private Guid _currentChatId; + private bool _disposed; + + public event Action MessageReceived; + public event Action MessageStatusUpdated; + public event Action TypingNotificationReceived; + public event Action ConnectionChanged; + + public ChatSignalRService(NavigationManager navigationManager, IIdentityService identityService) + { + _navigationManager = navigationManager; + _identityService = identityService; + } + + public async Task StartAsync(Guid userId, Guid currentChatId) + { + if (_hubConnection != null && _hubConnection.State == HubConnectionState.Connected) + { + await _hubConnection.StopAsync(); + } + + _userId = userId; + _currentChatId = currentChatId; + var hubUrl = $"http://localhost:5016/chatHub?userId={userId}&chatId={currentChatId}"; + + _hubConnection = new HubConnectionBuilder() + .WithUrl(hubUrl, options => + { + options.AccessTokenProvider = async () => + { + if (_disposed) + { + return null; + } + + try + { + return await _identityService.GetAccessTokenAsync(); + } + catch (JSDisconnectedException) + { + return null; + } + }; + }) + .WithAutomaticReconnect() + .Build(); + + RegisterHubEvents(); + await StartConnectionAsync(); + } + + public async Task StartAsync(Guid userId) + { + if (_hubConnection != null && _hubConnection.State == HubConnectionState.Connected) + { + await _hubConnection.StopAsync(); + } + + _userId = userId; + _currentChatId = Guid.Empty; + var hubUrl = $"http://localhost:5016/chatHub?userId={userId}"; + + _hubConnection = new HubConnectionBuilder() + .WithUrl(hubUrl, options => + { + options.AccessTokenProvider = async () => + { + if (_disposed) + { + return null; + } + + try + { + return await _identityService.GetAccessTokenAsync(); + } + catch (JSDisconnectedException) + { + return null; + } + }; + }) + .WithAutomaticReconnect() + .Build(); + + RegisterHubEvents(); + await StartConnectionAsync(); + } + + private void RegisterHubEvents() + { + _hubConnection.On("ReceiveMessage", (jsonMessage) => + { + var message = System.Text.Json.JsonSerializer.Deserialize(jsonMessage); + MessageReceived?.Invoke(message); + }); + + _hubConnection.On("ReceiveMessageStatusUpdate", (jsonStatusUpdate) => + { + var statusUpdate = System.Text.Json.JsonSerializer.Deserialize(jsonStatusUpdate); + MessageStatusUpdated?.Invoke(Guid.Parse(statusUpdate.MessageId), statusUpdate.Status); + }); + + _hubConnection.On("ReceiveTypingNotification", (userId, isTyping) => + { + TypingNotificationReceived?.Invoke(userId, isTyping); + }); + + _hubConnection.Reconnecting += (error) => + { + ConnectionChanged?.Invoke(false); + return Task.CompletedTask; + }; + + _hubConnection.Reconnected += (connectionId) => + { + ConnectionChanged?.Invoke(true); + return Task.CompletedTask; + }; + + _hubConnection.Closed += (error) => + { + ConnectionChanged?.Invoke(false); + return Task.CompletedTask; + }; + } + + private async Task StartConnectionAsync() + { + if (!_disposed) + { + await _hubConnection.StartAsync(); + ConnectionChanged?.Invoke(true); + } + } + + public async Task StopAsync() + { + if (_hubConnection != null && _hubConnection.State != HubConnectionState.Disconnected) + { + await _hubConnection.StopAsync(); + } + } + + public async Task SendTypingNotificationAsync(bool isTyping) + { + if (_hubConnection.State == HubConnectionState.Connected) + { + await _hubConnection.InvokeAsync("SendTypingNotification", _currentChatId.ToString(), _userId.ToString(), isTyping); + } + } + + public async ValueTask DisposeAsync() + { + if (_hubConnection != null) + { + await _hubConnection.DisposeAsync(); + } + _disposed = true; + } + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Communication/CommandsDto/AddUserToChatCommand.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Communication/CommandsDto/AddUserToChatCommand.cs new file mode 100644 index 000000000..398416ae8 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Communication/CommandsDto/AddUserToChatCommand.cs @@ -0,0 +1,16 @@ +using System; + +namespace MiniSpace.Web.Areas.Communication.CommandsDto +{ + public class AddUserToChatCommand + { + public Guid ChatId { get; set; } + public Guid UserId { get; set; } + + public AddUserToChatCommand(Guid chatId, Guid userId) + { + ChatId = chatId; + UserId = userId; + } + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Communication/CommandsDto/CreateChatCommand.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Communication/CommandsDto/CreateChatCommand.cs new file mode 100644 index 000000000..8e4c0ef4c --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Communication/CommandsDto/CreateChatCommand.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; + +namespace MiniSpace.Web.Areas.Communication.CommandsDto +{ + public class CreateChatCommand + { + public Guid ChatId { get; set; } + public List ParticipantIds { get; set; } + public string ChatName { get; set; } + + public CreateChatCommand(Guid chatId, List participantIds, string chatName = null) + { + ChatId = chatId; + ParticipantIds = participantIds ?? new List(); + ChatName = chatName; + } + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Communication/CommandsDto/DeleteChatCommand.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Communication/CommandsDto/DeleteChatCommand.cs new file mode 100644 index 000000000..2ed0047c3 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Communication/CommandsDto/DeleteChatCommand.cs @@ -0,0 +1,16 @@ +using System; + +namespace MiniSpace.Web.Areas.Communication.CommandsDto +{ + public class DeleteChatCommand + { + public Guid ChatId { get; set; } + public Guid UserId { get; set; } + + public DeleteChatCommand(Guid chatId, Guid userId) + { + ChatId = chatId; + UserId = userId; + } + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Communication/CommandsDto/DeleteMessageCommand.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Communication/CommandsDto/DeleteMessageCommand.cs new file mode 100644 index 000000000..855244213 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Communication/CommandsDto/DeleteMessageCommand.cs @@ -0,0 +1,16 @@ +using System; + +namespace MiniSpace.Web.Areas.Communication.CommandsDto +{ + public class DeleteMessageCommand + { + public Guid MessageId { get; set; } + public Guid ChatId { get; set; } + + public DeleteMessageCommand(Guid messageId, Guid chatId) + { + MessageId = messageId; + ChatId = chatId; + } + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Communication/CommandsDto/RemoveUserFromChatCommand.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Communication/CommandsDto/RemoveUserFromChatCommand.cs new file mode 100644 index 000000000..efe064eab --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Communication/CommandsDto/RemoveUserFromChatCommand.cs @@ -0,0 +1,16 @@ +using System; + +namespace MiniSpace.Web.Areas.Communication.CommandsDto +{ + public class RemoveUserFromChatCommand + { + public Guid ChatId { get; set; } + public Guid UserId { get; set; } + + public RemoveUserFromChatCommand(Guid chatId, Guid userId) + { + ChatId = chatId; + UserId = userId; + } + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Communication/CommandsDto/SendMessageCommand.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Communication/CommandsDto/SendMessageCommand.cs new file mode 100644 index 000000000..c6f842ec1 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Communication/CommandsDto/SendMessageCommand.cs @@ -0,0 +1,20 @@ +using System; + +namespace MiniSpace.Web.Areas.Communication.CommandsDto +{ + public class SendMessageCommand + { + public Guid ChatId { get; set; } + public Guid SenderId { get; set; } + public string Content { get; set; } + public string MessageType { get; set; } + + public SendMessageCommand(Guid chatId, Guid senderId, string content, string messageType = "Text") + { + ChatId = chatId; + SenderId = senderId; + Content = content; + MessageType = messageType; + } + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Communication/CommandsDto/UpdateMessageStatusCommand.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Communication/CommandsDto/UpdateMessageStatusCommand.cs new file mode 100644 index 000000000..5f71e6836 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Communication/CommandsDto/UpdateMessageStatusCommand.cs @@ -0,0 +1,18 @@ +using System; + +namespace MiniSpace.Web.Areas.Communication.CommandsDto +{ + public class UpdateMessageStatusCommand + { + public Guid ChatId { get; set; } + public Guid MessageId { get; set; } + public string Status { get; set; } + + public UpdateMessageStatusCommand(Guid chatId, Guid messageId, string status) + { + ChatId = chatId; + MessageId = messageId; + Status = status; + } + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Communication/CommunicationService.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Communication/CommunicationService.cs new file mode 100644 index 000000000..8e53cbfad --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Communication/CommunicationService.cs @@ -0,0 +1,103 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using MiniSpace.Web.DTO; +using MiniSpace.Web.HttpClients; +using MiniSpace.Web.DTO.Wrappers; +using MiniSpace.Web.Areas.Identity; +using MiniSpace.Web.DTO.Communication; +using MiniSpace.Web.Areas.Communication.CommandsDto; +using System.Linq; + +namespace MiniSpace.Web.Areas.Communication +{ + public class CommunicationService : ICommunicationService + { + private readonly IHttpClient _httpClient; + private readonly IIdentityService _identityService; + + public CommunicationService(IHttpClient httpClient, IIdentityService identityService) + { + _httpClient = httpClient; + _identityService = identityService; + } + + public async Task> GetUserChatsAsync(Guid userId, int page, int pageSize) + { + _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); + return await _httpClient.GetAsync>($"communication/chats/user/{userId}?page={page}&pageSize={pageSize}"); + } + + public async Task FindExistingChatAsync(Guid userId, Guid friendId) + { + var userChatsResponse = await GetUserChatsAsync(userId, 1, 100); + + if (userChatsResponse == null || !userChatsResponse.Items.Any()) + return null; + + // Loop through all the chats to find one with the friend + foreach (var userChat in userChatsResponse.Items.SelectMany(u => u.Chats)) + { + if (userChat.ParticipantIds.Contains(friendId)) + { + // Return the chat if a matching participant is found + return userChat; + } + } + + // Return null if no existing chat is found + return null; + } + + + public async Task GetChatByIdAsync(Guid chatId) + { + _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); + return await _httpClient.GetAsync($"communication/chats/{chatId}"); + } + + public async Task> GetMessagesForChatAsync(Guid chatId) + { + _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); + return await _httpClient.GetAsync>($"communication/chats/{chatId}/messages"); + } + + public async Task> CreateChatAsync(CreateChatCommand command) + { + _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); + return await _httpClient.PostAsync("communication/chats", command); + } + + public async Task AddUserToChatAsync(Guid chatId, Guid userId) + { + _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); + await _httpClient.PutAsync($"communication/chats/{chatId}/users", new { chatId, userId }); + } + + public async Task DeleteChatAsync(Guid chatId, Guid userId) + { + _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); + var command = new DeleteChatCommand(chatId, userId); + await _httpClient.DeleteAsync($"communication/chats/{chatId}/{userId}", command); + } + + + public async Task> SendMessageAsync(SendMessageCommand command) + { + _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); + return await _httpClient.PostAsync($"communication/chats/{command.ChatId}/messages", command); + } + + public async Task> UpdateMessageStatusAsync(UpdateMessageStatusCommand command) + { + _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); + return await _httpClient.PutAsync($"communication/chats/{command.ChatId}/messages/{command.MessageId}/status", command); + } + + public async Task DeleteMessageAsync(Guid chatId, Guid messageId) + { + _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); + await _httpClient.DeleteAsync($"communication/chats/{chatId}/messages/{messageId}"); + } + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Communication/ICommunicationService.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Communication/ICommunicationService.cs new file mode 100644 index 000000000..4ce6fdd7d --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Communication/ICommunicationService.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using MiniSpace.Web.Areas.Communication.CommandsDto; +using MiniSpace.Web.DTO; +using MiniSpace.Web.DTO.Communication; +using MiniSpace.Web.DTO.Wrappers; +using MiniSpace.Web.HttpClients; + +namespace MiniSpace.Web.Areas.Communication +{ + public interface ICommunicationService + { + Task> GetUserChatsAsync(Guid userId, int page, int pageSize); + Task FindExistingChatAsync(Guid userId, Guid friendId); + Task GetChatByIdAsync(Guid chatId); + Task> GetMessagesForChatAsync(Guid chatId); + Task> CreateChatAsync(CreateChatCommand command); + Task AddUserToChatAsync(Guid chatId, Guid userId); + Task DeleteChatAsync(Guid chatId, Guid userId); + Task> SendMessageAsync(SendMessageCommand command); + Task> UpdateMessageStatusAsync(UpdateMessageStatusCommand command); + Task DeleteMessageAsync(Guid chatId, Guid messageId); + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Events/CommandsDto/ViewEventCommand.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Events/CommandsDto/ViewEventCommand.cs new file mode 100644 index 000000000..47fa7a84e --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Events/CommandsDto/ViewEventCommand.cs @@ -0,0 +1,16 @@ +using System; + +namespace MiniSpace.Web.Areas.Events.CommandsDto +{ + public class ViewEventCommand + { + public Guid UserId { get; set; } + public Guid EventId { get; set; } + + public ViewEventCommand(Guid userId, Guid eventId) + { + UserId = userId; + EventId = eventId; + } + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Events/EventsService.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Events/EventsService.cs index f31120fc6..4d9eed430 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Areas/Events/EventsService.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Events/EventsService.cs @@ -83,12 +83,6 @@ public Task GetEventRatingAsync(Guid eventId) return _httpClient.GetAsync($"events/{eventId}/rating"); } - // public Task>>> SearchEventsAsync(SearchEvents command) - // { - // _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); - // return _httpClient.PostAsync>>("events/search", command); - // } - public Task> SearchEventsAsync(SearchEvents command) { _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); @@ -124,7 +118,6 @@ public Task> SearchEventsAsync(SearchEvents command) var queryString = "?" + string.Join("&", queryParams); - // Return the correct type based on your API response return _httpClient.GetAsync>($"events/search{queryString}"); } @@ -136,7 +129,6 @@ public Task>>> SearchOrganizerEve return _httpClient.PostAsync>>("events/search/organizer", command); } - // Implementations for participant-related methods public Task GetEventParticipantsAsync(Guid eventId) { _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); @@ -155,14 +147,14 @@ public Task RemoveEventParticipantAsync(Guid eventId, Guid participantId) return _httpClient.DeleteAsync($"events/{eventId}/participants?participantId={participantId}"); } - public Task> GetPaginatedEventsAsync(int page, int pageSize) + public Task> GetPaginatedEventsAsync(int page, int pageSize) { _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); - return _httpClient.GetAsync>($"events/paginated?page={page}&pageSize={pageSize}"); + return _httpClient.GetAsync>($"events/paginated?page={page}&pageSize={pageSize}"); } public async Task> GetMyEventsAsync(Guid organizerId, int page, int pageSize) { - return await _httpClient.GetAsync>($"events/organizer/{organizerId}/paginated?page={page}&pageSize={pageSize}"); + return await _httpClient.GetAsync>($"events/organizer/{organizerId}/paginated?page={page}&pageSize={pageSize}"); } public async Task> GetUserEventsAsync(Guid userId, int page, int pageSize, string engagementType) @@ -171,5 +163,18 @@ public async Task> GetUserEventsAsync(Guid userId, int pag return await _httpClient.GetAsync>($"events/users/{userId}?engagementType={engagementType}&page={page}&pageSize={pageSize}"); } + public async Task> GetUserEventsFeedAsync(Guid userId, int pageNumber, int pageSize, string sortBy, string direction) + { + _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); + var queryString = $"?pageNumber={pageNumber}&pageSize={pageSize}&sortBy={HttpUtility.UrlEncode(sortBy)}&direction={HttpUtility.UrlEncode(direction)}"; + return await _httpClient.GetAsync>($"events/users/{userId}/feed{queryString}"); + } + + public Task ViewEventAsync(ViewEventCommand command) + { + _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); + return _httpClient.PostAsync($"events/{command.EventId}/view", command); + } + } } diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Events/IEventsService.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Events/IEventsService.cs index 4bd880b55..dd96637f9 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Areas/Events/IEventsService.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Events/IEventsService.cs @@ -31,5 +31,7 @@ public interface IEventsService Task> GetPaginatedEventsAsync(int page, int pageSize); Task> GetMyEventsAsync(Guid organizerId, int page, int pageSize); Task> GetUserEventsAsync(Guid userId, int page, int pageSize, string engagementType); + Task> GetUserEventsFeedAsync(Guid userId, int pageNumber, int pageSize, string sortBy, string direction); + Task ViewEventAsync(ViewEventCommand command); } } diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Friends/FriendsService.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Friends/FriendsService.cs index 2c55781c4..3fe2d6341 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Areas/Friends/FriendsService.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Friends/FriendsService.cs @@ -175,6 +175,42 @@ public async Task> GetIncomingFriendRequestsAsync( return new PagedResult(incomingRequests, studentRequests.Page, studentRequests.PageSize, studentRequests.TotalItems); } + public async Task> GetPagedFollowersAsync(Guid userId, int page = 1, int pageSize = 10) + { + string accessToken = await _identityService.GetAccessTokenAsync(); + _httpClient.SetAccessToken(accessToken); + + string url = $"friends/{userId}/followers?page={page}&pageSize={pageSize}"; + var userFollowers = await _httpClient.GetAsync>(url); + + var allFollowers = userFollowers.Items.SelectMany(uf => uf.Friends).ToList(); + + foreach (var follower in allFollowers) + { + follower.StudentDetails = await GetStudentAsync(follower.UserId); + } + + return new PagedResult(allFollowers, userFollowers.Page, userFollowers.PageSize, userFollowers.TotalItems); + } + + public async Task> GetPagedFollowingAsync(Guid userId, int page = 1, int pageSize = 10) + { + string accessToken = await _identityService.GetAccessTokenAsync(); + _httpClient.SetAccessToken(accessToken); + + string url = $"friends/{userId}/following?page={page}&pageSize={pageSize}"; + var userFollowing = await _httpClient.GetAsync>(url); + + var allFollowing = userFollowing.Items.SelectMany(uf => uf.Friends).ToList(); + + foreach (var following in allFollowing) + { + following.StudentDetails = await GetStudentAsync(following.FriendId); + } + + return new PagedResult(allFollowing, userFollowing.Page, userFollowing.PageSize, userFollowing.TotalItems); + } + public async Task AcceptFriendRequestAsync(FriendRequestActionDto requestAction) { string accessToken = await _identityService.GetAccessTokenAsync(); diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Friends/IFriendsService.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Friends/IFriendsService.cs index 4c8f946b5..7ec78463d 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Areas/Friends/IFriendsService.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Friends/IFriendsService.cs @@ -21,6 +21,10 @@ public interface IFriendsService Task> GetAllFriendsAsync(Guid userId, int page = 1, int pageSize = 10); + Task> GetPagedFollowersAsync(Guid userId, int page = 1, int pageSize = 10); + + Task> GetPagedFollowingAsync(Guid userId, int page = 1, int pageSize = 10); + Task> AddFriendAsync(Guid friendId); Task RemoveFriendAsync(Guid friendId); diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Identity/IIdentityService.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Identity/IIdentityService.cs index 3938c5a29..136368d23 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Areas/Identity/IIdentityService.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Identity/IIdentityService.cs @@ -13,7 +13,7 @@ public interface IIdentityService bool IsAuthenticated { get; set; } Task GetAccountAsync(JwtDto jwtDto); Task> SignUpAsync(string firstName, string lastName, string email, string password, string role = "user", IEnumerable permissions = null); - Task> SignInAsync(string email, string password); + Task> SignInAsync(string email, string password, string deviceType); Task Logout(); Task GetAccessTokenAsync(); Task InitializeAuthenticationState(); diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Identity/IdentityComponent.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Identity/IdentityComponent.cs index 6e4ee255f..34d77e961 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Areas/Identity/IdentityComponent.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Identity/IdentityComponent.cs @@ -21,8 +21,8 @@ public Task OnInit() public Task SignUpAsync(string firstName, string lastName, string email, string password, string role = "user") => _identityService.SignUpAsync(firstName, lastName, email, password, role); - public Task> SignInAsync(string email, string password) - => _identityService.SignInAsync(email, password); + public Task> SignInAsync(string email, string password, string deviceType) + => _identityService.SignInAsync(email, password, deviceType); public Task GetAccount(JwtDto jwtDto) => _identityService.GetAccountAsync(jwtDto); diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Identity/IdentityService.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Identity/IdentityService.cs index 924b1cf72..eae45317d 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Areas/Identity/IdentityService.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Identity/IdentityService.cs @@ -48,9 +48,11 @@ public async Task> SignUpAsync(string firstName, string las new { firstName, lastName, email, password, role, permissions }); } - public async Task> SignInAsync(string email, string password) + public async Task> SignInAsync(string email, string password, string deviceType) { - var response = await _httpClient.PostAsync("identity/sign-in", new { email, password }); + var response = await _httpClient.PostAsync("identity/sign-in", + new { email, password, deviceType }); + if (response.Content != null) { JwtDto = response.Content; @@ -78,6 +80,7 @@ public async Task> SignInAsync(string email, string passwor return response; } + public async Task Logout() { if (JwtDto != null && !string.IsNullOrEmpty(JwtDto.RefreshToken)) diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Notifications/INotificationsService.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Notifications/INotificationsService.cs index d0836be89..9dc57534d 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Areas/Notifications/INotificationsService.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Notifications/INotificationsService.cs @@ -17,5 +17,6 @@ public interface INotificationsService Task DeleteNotificationAsync(Guid userId, Guid notificationId); Task GetNotificationByIdAsync(Guid userId, Guid notificationId); Task CreateNotificationAsync(NotificationToUsersDto notification); + Task IsUserConnectedAsync(Guid userId); } } diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Notifications/NotificationsService.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Notifications/NotificationsService.cs index 5e88341b7..9e0820d5b 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Areas/Notifications/NotificationsService.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Notifications/NotificationsService.cs @@ -8,6 +8,7 @@ using MiniSpace.Web.HttpClients; using Blazorise; using MiniSpace.Web.DTO.Notifications; +using System.Collections.Concurrent; namespace MiniSpace.Web.Areas.Notifications { @@ -16,6 +17,9 @@ public class NotificationsService: INotificationsService private readonly IHttpClient _httpClient; private readonly IIdentityService _identityService; + private static readonly ConcurrentDictionary ConnectedUsers = new(); + + public NotificationsService(IHttpClient httpClient, IIdentityService identityService) { _httpClient = httpClient; @@ -93,5 +97,22 @@ public async Task CreateNotificationAsync(NotificationToUsersDto notification) await _httpClient.PostAsync>(url, notification); } + public void AddConnectedUser(Guid userId) + { + ConnectedUsers[userId] = true; + } + + // Method to remove a user from the connected users list + public void RemoveConnectedUser(Guid userId) + { + ConnectedUsers.TryRemove(userId, out _); + } + + // Method to check if a user is connected + public Task IsUserConnectedAsync(Guid userId) + { + return Task.FromResult(ConnectedUsers.ContainsKey(userId)); + } + } } \ No newline at end of file diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Organizations/IOrganizationsService.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Organizations/IOrganizationsService.cs index 9cfc3522a..097369fed 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Areas/Organizations/IOrganizationsService.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Organizations/IOrganizationsService.cs @@ -35,5 +35,6 @@ public interface IOrganizationsService Task AcceptFollowRequestAsync(Guid organizationId, Guid requestId); Task RejectFollowRequestAsync(Guid organizationId, Guid requestId, string reason); Task> GetUserFollowedOrganizationsAsync(Guid userId); + Task> GetOrganizationRequestsAsync(Guid organizationId, int page, int pageSize); } } diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Organizations/OrganizationsService.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Organizations/OrganizationsService.cs index 94e203ccf..80bcf6b3d 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Areas/Organizations/OrganizationsService.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Organizations/OrganizationsService.cs @@ -187,7 +187,14 @@ public Task RejectFollowRequestAsync(Guid organizationId, Guid requestId, string public Task> GetUserFollowedOrganizationsAsync(Guid userId) { _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); - return _httpClient.GetAsync>($"users/{userId}/organizations/follow"); + return _httpClient.GetAsync>($"organizations/users/{userId}/organizations/follow"); + } + + public async Task> GetOrganizationRequestsAsync(Guid organizationId, int page, int pageSize) + { + _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); + var queryString = $"organizations/{organizationId}/requests?page={page}&pageSize={pageSize}"; + return await _httpClient.GetAsync>(queryString); } } } diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Students/CommandsDto/ViewUserProfileCommand.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Students/CommandsDto/ViewUserProfileCommand.cs new file mode 100644 index 000000000..a7f9b7d55 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Students/CommandsDto/ViewUserProfileCommand.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace MiniSpace.Web.Areas.Students.CommandsDto +{ + public class ViewUserProfileCommand + { + public Guid UserId { get; } + public Guid UserProfileId { get; } + + public ViewUserProfileCommand(Guid userId, Guid userProfileId) + { + UserId = userId; + UserProfileId = userProfileId; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Students/IStudentsService.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Students/IStudentsService.cs index cf5105ca8..d8c1c2d18 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Areas/Students/IStudentsService.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Students/IStudentsService.cs @@ -4,6 +4,9 @@ using MiniSpace.Web.DTO; using MiniSpace.Web.DTO.Interests; using MiniSpace.Web.DTO.Languages; +using MiniSpace.Web.DTO.Users; +using MiniSpace.Web.DTO.Views; +using MiniSpace.Web.DTO.Wrappers; using MiniSpace.Web.HttpClients; namespace MiniSpace.Web.Areas.Students @@ -48,5 +51,13 @@ Task UpdateStudentLanguagesAndInterestsAsync( IEnumerable languages, IEnumerable interests); + Task IsUserOnlineAsync(Guid studentId); + + Task ViewUserProfileAsync(Guid userId, Guid userProfileId); + + Task> GetUserProfileViewsAsync(Guid userId, int pageNumber, int pageSize); + Task BlockUserAsync(Guid blockerId, Guid blockedUserId); + Task UnblockUserAsync(Guid blockerId, Guid blockedUserId); + Task> GetBlockedUsersAsync(Guid blockerId, int page, int resultsPerPage); } } diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Students/StudentsService.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Students/StudentsService.cs index 7f963a115..4a59ddcc8 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Areas/Students/StudentsService.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Students/StudentsService.cs @@ -5,9 +5,14 @@ using System.Text.Json; using System.Threading.Tasks; using MiniSpace.Web.Areas.Identity; +using MiniSpace.Web.Areas.Notifications; +using MiniSpace.Web.Areas.Students.CommandsDto; using MiniSpace.Web.DTO; using MiniSpace.Web.DTO.Interests; using MiniSpace.Web.DTO.Languages; +using MiniSpace.Web.DTO.Users; +using MiniSpace.Web.DTO.Views; +using MiniSpace.Web.DTO.Wrappers; using MiniSpace.Web.HttpClients; namespace MiniSpace.Web.Areas.Students @@ -17,6 +22,8 @@ public class StudentsService : IStudentsService private readonly IHttpClient _httpClient; private readonly IIdentityService _identityService; + private readonly INotificationsService _notificationsService; + public StudentDto StudentDto { get; private set; } public StudentsService(IHttpClient httpClient, IIdentityService identityService) @@ -203,5 +210,55 @@ public async Task UpdateStudentLanguagesAndInterestsAsync( await _httpClient.PutAsync($"students/{studentId}/languages-and-interests", updateData); } + + public async Task IsUserOnlineAsync(Guid studentId) + { + return await _notificationsService.IsUserConnectedAsync(studentId); + } + + public async Task ViewUserProfileAsync(Guid userId, Guid userProfileId) + { + var accessToken = await _identityService.GetAccessTokenAsync(); + _httpClient.SetAccessToken(accessToken); + + var command = new ViewUserProfileCommand(userId, userProfileId); + await _httpClient.PostAsync("students/profiles/users/{userProfileId}/view", command); + } + + public async Task> GetUserProfileViewsAsync(Guid userId, int pageNumber, int pageSize) + { + var accessToken = await _identityService.GetAccessTokenAsync(); + _httpClient.SetAccessToken(accessToken); + + var queryString = $"?pageNumber={pageNumber}&pageSize={pageSize}"; + return await _httpClient.GetAsync>($"students/profiles/users/{userId}/views/paginated{queryString}"); + } + + public async Task BlockUserAsync(Guid blockerId, Guid blockedUserId) + { + var accessToken = await _identityService.GetAccessTokenAsync(); + _httpClient.SetAccessToken(accessToken); + + var command = new { blockerId, blockedUserId }; + await _httpClient.PostAsync($"students/{blockerId}/block-user/{blockedUserId}", command); + } + + public async Task UnblockUserAsync(Guid blockerId, Guid blockedUserId) + { + var accessToken = await _identityService.GetAccessTokenAsync(); + _httpClient.SetAccessToken(accessToken); + + var command = new { blockerId, blockedUserId }; + await _httpClient.PostAsync($"students/{blockerId}/unblock-user/{blockedUserId}", command); + } + + public async Task> GetBlockedUsersAsync(Guid blockerId, int page, int resultsPerPage) + { + var accessToken = await _identityService.GetAccessTokenAsync(); + _httpClient.SetAccessToken(accessToken); + + var queryString = $"?page={page}&resultsPerPage={resultsPerPage}"; + return await _httpClient.GetAsync>($"students/{blockerId}/blocked-users{queryString}"); + } } } diff --git a/MiniSpace.Web/src/MiniSpace.Web/DTO/Communication/ChatDto.cs b/MiniSpace.Web/src/MiniSpace.Web/DTO/Communication/ChatDto.cs new file mode 100644 index 000000000..5a267c24a --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/DTO/Communication/ChatDto.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; + +namespace MiniSpace.Web.DTO.Communication +{ + public class ChatDto + { + public Guid Id { get; set; } + public string Name { get; set; } + public List ParticipantIds { get; set; } + public List Messages { get; set; } + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/DTO/Communication/MessageDto.cs b/MiniSpace.Web/src/MiniSpace.Web/DTO/Communication/MessageDto.cs new file mode 100644 index 000000000..a5dac8538 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/DTO/Communication/MessageDto.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; + +namespace MiniSpace.Web.DTO.Communication +{ + public class MessageDto + { + public Guid Id { get; set; } + public Guid SenderId { get; set; } + public string Content { get; set; } + public Guid ChatId { get; set; } + public DateTime Timestamp { get; set; } + public string MessageType { get; set; } + public string Status { get; set; } + } + +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/DTO/Communication/MessageStatusUpdateDto.cs b/MiniSpace.Web/src/MiniSpace.Web/DTO/Communication/MessageStatusUpdateDto.cs new file mode 100644 index 000000000..4cb68892b --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/DTO/Communication/MessageStatusUpdateDto.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; + +namespace MiniSpace.Web.DTO.Communication +{ + public class MessageStatusUpdateDto + { + public string ChatId { get; set; } + public string MessageId { get; set; } + public string Status { get; set; } + } + +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/DTO/Communication/UserChatDto.cs b/MiniSpace.Web/src/MiniSpace.Web/DTO/Communication/UserChatDto.cs new file mode 100644 index 000000000..a7cefacac --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/DTO/Communication/UserChatDto.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; + +namespace MiniSpace.Web.DTO.Communication +{ + public class UserChatDto + { + public Guid UserId { get; set; } + public List Chats { get; set; } + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/DTO/Organizations/OrganizationDto.cs b/MiniSpace.Web/src/MiniSpace.Web/DTO/Organizations/OrganizationDto.cs index b089a51a5..b52139fb6 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/DTO/Organizations/OrganizationDto.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/DTO/Organizations/OrganizationDto.cs @@ -23,6 +23,7 @@ public class OrganizationDto public string Email { get; set; } public IEnumerable Users { get; set; } = new List(); + public OrganizationSettingsDto Settings { get; set; } = new OrganizationSettingsDto(); public int UserCount => Users?.Count() ?? 0; public bool IsExpanded { get; set; } = false; diff --git a/MiniSpace.Web/src/MiniSpace.Web/DTO/Organizations/OrganizationRequestDto.cs b/MiniSpace.Web/src/MiniSpace.Web/DTO/Organizations/OrganizationRequestDto.cs new file mode 100644 index 000000000..471511760 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/DTO/Organizations/OrganizationRequestDto.cs @@ -0,0 +1,13 @@ +using System; + +namespace MiniSpace.Web.DTO.Organizations +{ + public class OrganizationRequestDto + { + public Guid RequestId { get; set; } + public Guid UserId { get; set; } + public DateTime RequestDate { get; set; } + public string State { get; set; } + public string Reason { get; set; } + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/DTO/Organizations/OrganizationRequestsDto.cs b/MiniSpace.Web/src/MiniSpace.Web/DTO/Organizations/OrganizationRequestsDto.cs new file mode 100644 index 000000000..10bedfd2e --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/DTO/Organizations/OrganizationRequestsDto.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; + +namespace MiniSpace.Web.DTO.Organizations +{ + public class OrganizationRequestsDto + { + public Guid OrganizationId { get; set; } + public IEnumerable Requests { get; set; } + + public OrganizationRequestsDto() + { + Requests = new List(); + } + + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/DTO/Users/BlockedUserDto.cs b/MiniSpace.Web/src/MiniSpace.Web/DTO/Users/BlockedUserDto.cs new file mode 100644 index 000000000..d12c24c53 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/DTO/Users/BlockedUserDto.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace MiniSpace.Web.DTO.Users +{ + public class BlockedUserDto + { + public Guid BlockerId { get; set; } + public Guid BlockedUserId { get; set; } + public DateTime BlockedAt { get; set; } + } +} \ No newline at end of file diff --git a/MiniSpace.Web/src/MiniSpace.Web/DTO/Views/UserProfileViewDto.cs b/MiniSpace.Web/src/MiniSpace.Web/DTO/Views/UserProfileViewDto.cs new file mode 100644 index 000000000..7ec2ae91e --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/DTO/Views/UserProfileViewDto.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace MiniSpace.Web.DTO.Views +{ + public class UserProfileViewDto + { + public Guid UserProfileId { get; set; } + public DateTime Date { get; set; } + public string IpAddress { get; set; } + public string DeviceType { get; set; } + public string OperatingSystem { get; set; } + } +} \ No newline at end of file diff --git a/MiniSpace.Web/src/MiniSpace.Web/DTO/Wrappers/PagedResponseDto.cs b/MiniSpace.Web/src/MiniSpace.Web/DTO/Wrappers/PagedResponseDto.cs index 0c16846a1..06f4ec9d1 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/DTO/Wrappers/PagedResponseDto.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/DTO/Wrappers/PagedResponseDto.cs @@ -5,20 +5,16 @@ namespace MiniSpace.Web.DTO.Wrappers { public class PagedResponseDto : ResponseDto { - public IEnumerable Items { get; } - public int TotalPages { get; } - public int TotalItems { get; } - public int PageSize { get; } - public int Page { get; } - public bool First { get; } - public bool Last { get; } - public bool Empty { get; } + public IEnumerable Items { get; set; } = new List(); + public int TotalPages { get; set; } + public int TotalItems { get; set; } + public int PageSize { get; set; } + public int Page { get; set; } + public bool First { get; set; } + public bool Last { get; set; } + public bool Empty { get; set; } public int? NextPage => Page < TotalPages ? Page + 1 : (int?)null; public int? PreviousPage => Page > 1 ? Page - 1 : (int?)null; - - public PagedResponseDto() - { - Items = new List(); - } } + } \ No newline at end of file diff --git a/MiniSpace.Web/src/MiniSpace.Web/Models/BlockedUsers/BlockedUserViewModel.cs b/MiniSpace.Web/src/MiniSpace.Web/Models/BlockedUsers/BlockedUserViewModel.cs new file mode 100644 index 000000000..59072dd4a --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Models/BlockedUsers/BlockedUserViewModel.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace MiniSpace.Web.Models.BlockedUsers +{ + public class BlockedUserViewModel + { + public Guid BlockedUserId { get; set; } + public string FullName { get; set; } + public string ProfileImageUrl { get; set; } + public DateTime BlockedAt { get; set; } + } +} \ No newline at end of file diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Account/BlockedListComponent.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Account/BlockedListComponent.razor new file mode 100644 index 000000000..5b605b16d --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Account/BlockedListComponent.razor @@ -0,0 +1,145 @@ +@page "/blocked-users" +@inject IStudentsService StudentsService +@inject IIdentityService IdentityService +@inject ISnackbar Snackbar +@inject NavigationManager NavigationManager +@using MiniSpace.Web.DTO.Users +@using MiniSpace.Web.Models.BlockedUsers +@using MudBlazor +@using System.Collections.Generic +@using System.Threading.Tasks + + + + Blocked Users + @if (isLoading) + { + + } + else + { + @if (blockedUsers?.Any() == true) + { + @foreach (var user in blockedUsers) + { + + + + + + + + + + + @user.FullName + Blocked on: @user.BlockedAt.ToString("MMMM dd, yyyy") + + + + + +
+ + Unblock + +
+
+
+
+
+ } + } + else + { + You have no blocked users. + } + } +
+ + + +@code { + private List blockedUsers = new(); + private bool isLoading = true; + + protected override async Task OnInitializedAsync() + { + isLoading = true; + await LoadBlockedUsersAsync(); + isLoading = false; + } + + private async Task LoadBlockedUsersAsync() + { + try + { + var currentUserId = IdentityService.GetCurrentUserId(); + var response = await StudentsService.GetBlockedUsersAsync(currentUserId, 1, 10); + var blockedUserIds = response.Items.Select(bu => bu.BlockedUserId).ToList(); + + foreach (var blockedUserId in blockedUserIds) + { + var student = await StudentsService.GetStudentAsync(blockedUserId); + if (student != null) + { + blockedUsers.Add(new BlockedUserViewModel + { + BlockedUserId = blockedUserId, + FullName = $"{student.FirstName} {student.LastName}", + ProfileImageUrl = GetProfileImageUrl(student.ProfileImageUrl), + BlockedAt = response.Items.First(bu => bu.BlockedUserId == blockedUserId).BlockedAt + }); + } + } + } + catch (Exception ex) + { + Snackbar.Add($"Error loading blocked users: {ex.Message}", Severity.Error); + } + } + + private async Task UnblockUser(Guid blockedUserId) + { + try + { + var currentUserId = IdentityService.GetCurrentUserId(); + await StudentsService.UnblockUserAsync(currentUserId, blockedUserId); + blockedUsers.RemoveAll(u => u.BlockedUserId == blockedUserId); + Snackbar.Add("User has been unblocked successfully.", Severity.Success); + StateHasChanged(); + } + catch (Exception ex) + { + Snackbar.Add($"Error unblocking user: {ex.Message}", Severity.Error); + } + } + + private string GetProfileImageUrl(string profileImageUrl) + { + return string.IsNullOrEmpty(profileImageUrl) ? "images/default_profile_image.webp" : profileImageUrl; + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Account/ProfileComponent.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Account/ProfileComponent.razor index 4c3ba0bc7..5a3fb082a 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Pages/Account/ProfileComponent.razor +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Account/ProfileComponent.razor @@ -129,7 +129,7 @@ else } - + Add Education @@ -169,7 +169,7 @@ else } - + Add Work Experience diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Account/ShowAccount.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Account/ShowAccount.razor index 5eb22dbb2..535a4c8e6 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Pages/Account/ShowAccount.razor +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Account/ShowAccount.razor @@ -30,6 +30,7 @@ Languages & Interests Gallery User Settings + Blocked Users @@ -92,6 +93,10 @@ { } + else if (activeTabIndex == 7) + { + + } @@ -157,7 +162,7 @@ @code { private List _items = new List { - new BreadcrumbItem("Home", href: "/", icon: Icons.Material.Filled.Home), + new BreadcrumbItem("Home", href: "/home", icon: Icons.Material.Filled.Home), new BreadcrumbItem("Account settings", href: "/events/follow", disabled: true, icon: @Icons.Material.Filled.ManageAccounts), }; diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Account/SignIn.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Account/SignIn.razor index 4a60ec66c..b71b282fa 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Pages/Account/SignIn.razor +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Account/SignIn.razor @@ -11,12 +11,10 @@ @inject NavigationManager NavigationManager @inject IJSRuntime JSRuntime - + +@code { + [Parameter] public Guid ChatId { get; set; } + + private List userChats = new(); + private List messages = new(); + private string newMessageContent = string.Empty; + private Dictionary userNames = new(); + private Dictionary userImages = new(); + private Dictionary lastMessages = new(); + private Dictionary typingStatus = new(); + private bool isSending = false; + private bool hasUpdatedStatus = false; + private bool isUserTyping = false; + private string typingUserName = string.Empty; + private Timer typingTimer; + private bool isConnected = false; // Start as not connected + private bool IsSendButtonDisabledCombined => IsSendButtonDisabled || !isConnected; + + private bool IsSendButtonDisabled => isSending || string.IsNullOrWhiteSpace(newMessageContent); + + protected override async Task OnInitializedAsync() + { + await IdentityService.InitializeAuthenticationState(); + + if (IdentityService.IsAuthenticated) + { + var userId = IdentityService.GetCurrentUserId(); + ChatSignalRService.ConnectionChanged += OnConnectionChanged; + ChatSignalRService.MessageReceived += OnMessageReceived; + ChatSignalRService.MessageStatusUpdated += OnMessageStatusUpdated; + ChatSignalRService.TypingNotificationReceived += OnTypingNotificationReceived; + + await InitializeSignalRConnection(userId); + + if (isConnected) + { + await LoadUserChats(); + if (ChatId != Guid.Empty) + { + await LoadMessages(ChatId); + } + } + else + { + NavigationManager.NavigateTo(NavigationManager.Uri, forceLoad: true); + } + } + else + { + NavigationManager.NavigateTo("/login"); + } + } + + protected override async Task OnParametersSetAsync() + { + if (isConnected) + { + await LoadMessages(ChatId); + } + } + + private async Task InitializeSignalRConnection(Guid userId) + { + if (!isConnected) + { + await ChatSignalRService.StartAsync(userId, ChatId); + } + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + await ScrollToBottomAsync(); + } + else + { + await JSRuntime.InvokeVoidAsync("scrollToBottom", "chatMessagesContainer"); + } + + if (!hasUpdatedStatus) + { + hasUpdatedStatus = true; + await Task.Delay(1000); + await UpdateUnreadMessagesStatusAsync(); + } + } + + private async void OnTypingTimeout(object state) + { + await InvokeAsync(() => + { + if (state is Guid chatId) + { + typingStatus[chatId] = false; + isUserTyping = false; + typingUserName = string.Empty; + StateHasChanged(); + } + }); + + typingTimer?.Dispose(); + } + + private async Task HandleInputChange(ChangeEventArgs e) + { + if (e.Value is string inputValue) + { + await ChatSignalRService.SendTypingNotificationAsync(!string.IsNullOrEmpty(inputValue)); + + typingTimer?.Dispose(); + typingTimer = new Timer(OnTypingTimeout, ChatId, 1000, Timeout.Infinite); + } + } + + private async void OnTypingNotificationReceived(string userId, bool isTyping) + { + await InvokeAsync(() => + { + var chatId = ChatId; + + if (userNames.TryGetValue(Guid.Parse(userId), out var userName)) + { + typingUserName = userName; + } + else + { + typingUserName = "Unknown User"; + } + + isUserTyping = isTyping; + typingStatus[chatId] = isTyping; + + if (isTyping) + { + typingTimer?.Dispose(); + typingTimer = new Timer(OnTypingTimeout, chatId, 1000, Timeout.Infinite); + } + + StateHasChanged(); + }); + } + + private async void OnConnectionChanged(bool connected) + { + await InvokeAsync(async () => + { + isConnected = connected; + if (isConnected) + { + await LoadUserChats(); + if (ChatId != Guid.Empty) + { + await LoadMessages(ChatId); + } + StateHasChanged(); + } + }); + } + + private async Task LoadUserChats() + { + try + { + var userId = IdentityService.GetCurrentUserId(); + var result = await CommunicationService.GetUserChatsAsync(userId, 1, 20); + + if (result != null) + { + userChats = result.Items.SelectMany(u => u.Chats).ToList(); + await LoadUserDetails(); + await LoadLastMessages(); + } + } + catch (Exception ex) + { + Snackbar.Add($"Failed to load chats: {ex.Message}", Severity.Error); + } + } + + private async Task LoadMessages(Guid chatId) + { + try + { + messages = (await CommunicationService.GetMessagesForChatAsync(chatId)).ToList(); + await LoadUserDetails(); + await ScrollToBottomAsync(); + } + catch (Exception ex) + { + Snackbar.Add($"Failed to load messages: {ex.Message}", Severity.Error); + } + } + + private async Task LoadUserDetails() + { + var senderIds = messages.Select(m => m.SenderId).Distinct().ToList(); + var chatUserIds = userChats.SelectMany(c => c.ParticipantIds).Distinct().ToList(); + var allUserIds = senderIds.Union(chatUserIds).Distinct().ToList(); + + foreach (var userId in allUserIds) + { + if (!userNames.ContainsKey(userId)) + { + var user = await StudentsService.GetStudentAsync(userId); + if (user != null) + { + userNames[userId] = $"{user.FirstName} {user.LastName}"; + userImages[userId] = string.IsNullOrWhiteSpace(user.ProfileImageUrl) ? "images/default_profile_image.webp" : user.ProfileImageUrl; + } + } + } + } + + private async Task LoadLastMessages() + { + foreach (var chat in userChats) + { + var messages = await CommunicationService.GetMessagesForChatAsync(chat.Id); + lastMessages[chat.Id] = messages.OrderByDescending(m => m.Timestamp).FirstOrDefault(); + } + StateHasChanged(); + } + + private async Task SendMessage() + { + if (!string.IsNullOrWhiteSpace(newMessageContent) && !isSending) + { + isSending = true; + + try + { + var userId = IdentityService.GetCurrentUserId(); + var command = new SendMessageCommand(ChatId, userId, newMessageContent); + + var response = await CommunicationService.SendMessageAsync(command); + if (response.IsSuccessStatusCode) + { + newMessageContent = string.Empty; + Snackbar.Add("Message sent!", Severity.Success); + await ScrollToBottomAsync(); + } + else + { + Snackbar.Add("Failed to send message.", Severity.Error); + } + } + catch (Exception ex) + { + Snackbar.Add($"Error: {ex.Message}", Severity.Error); + } + finally + { + isSending = false; + } + } + } + + private async Task ScrollToBottomAsync() + { + await JSRuntime.InvokeVoidAsync("scrollToBottom", "chatMessagesContainer"); + } + + private async Task UpdateUnreadMessagesStatusAsync() + { + var unreadMessages = messages + .Where(m => m.SenderId != IdentityService.GetCurrentUserId() && m.Status != "Read") + .ToList(); + + if (unreadMessages.Any()) + { + foreach (var message in unreadMessages) + { + await UpdateMessageStatus(message, "Read"); + } + } + } + + private async Task UpdateMessageStatus(MessageDto message, string status) + { + if (message.ChatId == Guid.Empty || message.Id == Guid.Empty) + { + Snackbar.Add("Invalid message ID or chat ID.", Severity.Error); + return; + } + + var command = new UpdateMessageStatusCommand(message.ChatId, message.Id, status); + var response = await CommunicationService.UpdateMessageStatusAsync(command); + + if (response.IsSuccessStatusCode) + { + message.Status = status; + } + else + { + Snackbar.Add($"Failed to update message status: {response.ErrorMessage}", Severity.Error); + } + } + + private void SelectChat(Guid chatId) + { + NavigationManager.NavigateTo($"/chats/{chatId}"); + } + + private string GetChatItemClass(Guid chatId) + { + return ChatId == chatId ? "selected-chat" : string.Empty; + } + + private int GetUnreadMessageCount(Guid chatId) + { + return messages.Count(m => m.ChatId == chatId && m.SenderId != IdentityService.GetCurrentUserId() && m.Status != "Read"); + } + + private string GetMessageBubbleClass(MessageDto message) + { + return message.SenderId == IdentityService.GetCurrentUserId() ? "sent" : "received"; + } + + private string GetSenderName(Guid senderId) + { + return userNames.TryGetValue(senderId, out var name) ? name : "Unknown"; + } + + private string GetSenderImage(Guid senderId) + { + return userImages.TryGetValue(senderId, out var imageUrl) ? imageUrl : "/images/default_profile_image.webp"; + } + + private string GetChatImage(Guid chatId) + { + if (IdentityService.IsAuthenticated) + { + var chat = userChats.FirstOrDefault(c => c.Id == chatId); + + if (chat == null) + { + return "/images/default_profile_image.webp"; + } + + var userId = IdentityService.GetCurrentUserId(); + if (chat.ParticipantIds.Count == 2) + { + var otherParticipantId = chat.ParticipantIds.FirstOrDefault(id => id != userId); + return GetSenderImage(otherParticipantId); + } + + var otherParticipant = chat.ParticipantIds.FirstOrDefault(id => id != userId); + if (otherParticipant != Guid.Empty) + { + return GetSenderImage(otherParticipant); + } + } + + return "/images/default_profile_image.webp"; + } + + private string GetChatName(Guid chatId) + { + var chat = userChats.FirstOrDefault(c => c.Id == chatId); + + if (IdentityService.IsAuthenticated) + { + var userId = IdentityService.GetCurrentUserId(); + if (chat != null && chat.ParticipantIds.Count == 2) + { + var otherParticipantId = chat.ParticipantIds.FirstOrDefault(id => id != userId); + return userNames.TryGetValue(otherParticipantId, out var otherParticipantName) + ? otherParticipantName + : "Unknown Chat"; + } + + return chat?.Name ?? "Unknown Chat"; + } + return "Unknown Chat"; + } + + private async void OnMessageReceived(MessageDto message) + { + await InvokeAsync(() => + { + if (message.ChatId == ChatId) + { + if (!messages.Any(m => m.Id == message.Id)) + { + messages.Add(message); + ScrollToBottomAsync(); + StateHasChanged(); + } + } + + if (lastMessages.ContainsKey(message.ChatId)) + { + lastMessages[message.ChatId] = message; + } + else + { + lastMessages.Add(message.ChatId, message); + } + + StateHasChanged(); + }); + } + + private async void OnMessageStatusUpdated(Guid messageId, string status) + { + await InvokeAsync(() => + { + var message = messages.FirstOrDefault(m => m.Id == messageId); + if (message != null) + { + message.Status = status; + StateHasChanged(); + } + }); + } + + private string GetLastMessagePreview(Guid chatId) + { + if (lastMessages.TryGetValue(chatId, out var lastMessage)) + { + return $"{GetSenderName(lastMessage.SenderId)}: {lastMessage.Content}"; + } + return "No messages yet"; + } + + private string GetLastMessageTime(Guid chatId) + { + if (lastMessages.TryGetValue(chatId, out var lastMessage)) + { + return lastMessage.Timestamp.ToString("g"); + } + return string.Empty; + } + + private bool IsUserTypingInChat(Guid chatId) + { + return typingStatus.TryGetValue(chatId, out var isTyping) && isTyping; + } + + private string GetStatusIcon(string status) + { + return status switch + { + "Sent" => Icons.Material.Filled.Check, + "Delivered" => Icons.Material.Filled.DoneAll, + "Read" => Icons.Material.Filled.Visibility, + _ => Icons.Material.Filled.Schedule // Default icon for pending or unknown status + }; + } + + public async ValueTask DisposeAsync() + { + ChatSignalRService.MessageReceived -= OnMessageReceived; + ChatSignalRService.MessageStatusUpdated -= OnMessageStatusUpdated; + ChatSignalRService.TypingNotificationReceived -= OnTypingNotificationReceived; + ChatSignalRService.ConnectionChanged -= OnConnectionChanged; + await ChatSignalRService.DisposeAsync(); + typingTimer?.Dispose(); + } +} \ No newline at end of file diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Chats/ChatsAll.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Chats/ChatsAll.razor new file mode 100644 index 000000000..0677efc1d --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Chats/ChatsAll.razor @@ -0,0 +1,411 @@ +@page "/chats/all" +@using MiniSpace.Web.HttpClients +@using MiniSpace.Web.Areas.Communication +@using MiniSpace.Web.DTO.Communication +@inject IIdentityService IdentityService +@inject IStudentsService StudentsService +@inject ICommunicationService CommunicationService +@inject NavigationManager NavigationManager +@inject ISnackbar Snackbar +@inject IDialogService DialogService +@inject ChatSignalRService ChatSignalRService +@using MudBlazor +@using System.Threading +@implements IAsyncDisposable + + + + + + + + @if (filteredChats.Any()) + { + @foreach (var chat in filteredChats) + { + var lastMessage = lastMessages.ContainsKey(chat.Id) ? lastMessages[chat.Id] : null; + var isTyping = typingStatus.ContainsKey(chat.Id); + + + + +
+ @GetChatName(chat.Id) + + @if (isTyping) + { + @typingStatus[chat.Id].UserName is typing... + } + else + { + @GetSenderName(lastMessage?.SenderId ?? Guid.Empty): @lastMessage?.Content + } + +
+ @lastMessage?.Timestamp.ToString("MMM d, h:mm tt") + + Delete Chat + +
+ + } + } + else + { + No chats found. + } +
+
+ + + +@code { + private List userChats = new(); + private Dictionary lastMessages = new(); + private Dictionary userNames = new(); + private Dictionary userImages = new(); + private Dictionary typingStatus = new(); + private string searchQuery = string.Empty; + + private IEnumerable filteredChats => userChats + .Where(chat => string.IsNullOrEmpty(searchQuery) || + GetChatName(chat.Id).Contains(searchQuery, StringComparison.OrdinalIgnoreCase) || + (lastMessages.ContainsKey(chat.Id) && lastMessages[chat.Id]?.Content.Contains(searchQuery, StringComparison.OrdinalIgnoreCase) == true)) + .ToList(); + + protected override async Task OnInitializedAsync() + { + await IdentityService.InitializeAuthenticationState(); + + if (IdentityService.IsAuthenticated) + { + await LoadUserChats(); + + var userId = IdentityService.GetCurrentUserId(); + await ChatSignalRService.StartAsync(userId); + ChatSignalRService.MessageReceived += OnMessageReceived; + ChatSignalRService.TypingNotificationReceived += OnTypingNotificationReceived; + } + else + { + NavigationManager.NavigateTo("/login"); + } + } + + private async Task LoadUserChats() + { + try + { + var userId = IdentityService.GetCurrentUserId(); + var result = await CommunicationService.GetUserChatsAsync(userId, 1, 20); + + if (result != null) + { + userChats = result.Items.SelectMany(u => u.Chats).ToList(); + await LoadLastMessages(); + await LoadUserDetails(); + + userChats = userChats + .OrderByDescending(c => lastMessages.ContainsKey(c.Id) ? lastMessages[c.Id]?.Timestamp : DateTime.MinValue) + .ToList(); + } + } + catch (Exception ex) + { + Snackbar.Add($"Failed to load chats: {ex.Message}", Severity.Error); + } + } + + private async Task LoadLastMessages() + { + foreach (var chat in userChats) + { + var messages = await CommunicationService.GetMessagesForChatAsync(chat.Id); + lastMessages[chat.Id] = messages.OrderByDescending(m => m.Timestamp).FirstOrDefault(); + } + } + + private async Task LoadUserDetails() + { + var allUserIds = userChats.SelectMany(c => c.ParticipantIds).Distinct().ToList(); + + foreach (var userId in allUserIds) + { + if (!userNames.ContainsKey(userId)) + { + var user = await StudentsService.GetStudentAsync(userId); + if (user != null) + { + userNames[userId] = $"{user.FirstName} {user.LastName}"; + userImages[userId] = string.IsNullOrWhiteSpace(user.ProfileImageUrl) ? "images/default_profile_image.webp" : user.ProfileImageUrl; + } + } + } + } + + private void SelectChat(Guid chatId) + { + NavigationManager.NavigateTo($"/chats/{chatId}"); + } + + private async Task ShowDeleteChatDialog(Guid chatId) + { + var parameters = new DialogParameters + { + ["Message"] = "Are you sure you want to delete this chat? This action cannot be undone." + }; + + var options = new DialogOptions + { + CloseOnEscapeKey = true, + MaxWidth = MaxWidth.Small, + CloseButton = true, + DisableBackdropClick = true, + ClassBackground = "mud-dialog-blur-backdrop" + }; + + var dialog = DialogService.Show("Delete Chat", parameters, options); + + var result = await dialog.Result; + + if (!result.Canceled) + { + await DeleteChat(chatId); + } + } + + private async Task DeleteChat(Guid chatId) + { + try + { + var userId = IdentityService.GetCurrentUserId(); + + await CommunicationService.DeleteChatAsync(chatId, userId); + + userChats = userChats.Where(c => c.Id != chatId).ToList(); + Snackbar.Add("Chat deleted successfully.", Severity.Success); + } + catch (Exception ex) + { + Snackbar.Add($"Failed to delete chat: {ex.Message}", Severity.Error); + } + } + + private string GetChatName(Guid chatId) + { + var chat = userChats.FirstOrDefault(c => c.Id == chatId); + + if (chat != null && chat.ParticipantIds.Count == 2) + { + var otherParticipantId = chat.ParticipantIds.FirstOrDefault(id => id != IdentityService.GetCurrentUserId()); + return userNames.TryGetValue(otherParticipantId, out var otherParticipantName) ? otherParticipantName : "Unknown Chat"; + } + + return chat?.Name ?? "Group Chat"; + } + + private string GetChatImage(Guid chatId) + { + var chat = userChats.FirstOrDefault(c => c.Id == chatId); + if (chat == null) return "/images/default_profile_image.webp"; + + var userId = IdentityService.GetCurrentUserId(); + if (chat.ParticipantIds.Count == 2) + { + var otherParticipantId = chat.ParticipantIds.FirstOrDefault(id => id != userId); + return GetSenderImage(otherParticipantId); + } + + return "/images/default_profile_image.webp"; + } + + private string GetSenderImage(Guid senderId) + { + return userImages.TryGetValue(senderId, out var imageUrl) ? imageUrl : "/images/default_profile_image.webp"; + } + + private string GetSenderName(Guid senderId) + { + return userNames.TryGetValue(senderId, out var name) ? name : "Unknown"; + } + + private async void OnMessageReceived(MessageDto message) + { + await InvokeAsync(async () => + { + // Update last message in chat + lastMessages[message.ChatId] = message; + + // Move chat to the top of the list + var chat = userChats.FirstOrDefault(c => c.Id == message.ChatId); + if (chat != null) + { + userChats.Remove(chat); + userChats.Insert(0, chat); + } + else + { + // Load chat if not found (e.g., was deleted locally) + await LoadUserChats(); + } + + StateHasChanged(); + }); + } + + private async void OnTypingNotificationReceived(string userId, bool isTyping) + { + await InvokeAsync(() => + { + var parsedUserId = Guid.Parse(userId); + var chatId = userChats.FirstOrDefault(c => c.ParticipantIds.Contains(parsedUserId))?.Id ?? Guid.Empty; + + if (chatId != Guid.Empty) + { + if (isTyping) + { + if (userNames.TryGetValue(parsedUserId, out var typingUserName)) + { + typingStatus[chatId] = (typingUserName, new Timer(OnTypingTimeout, chatId, 1000, Timeout.Infinite)); + } + } + else + { + if (typingStatus.TryGetValue(chatId, out var typingEntry)) + { + typingEntry.Timer.Dispose(); + typingStatus.Remove(chatId); + } + } + + StateHasChanged(); + } + }); + } + + private void OnTypingTimeout(object state) + { + if (state is Guid chatId && typingStatus.TryGetValue(chatId, out var typingEntry)) + { + typingEntry.Timer.Dispose(); + typingStatus.Remove(chatId); + + InvokeAsync(() => StateHasChanged()); + } + } + + public async ValueTask DisposeAsync() + { + foreach (var (_, timer) in typingStatus.Values) + { + timer.Dispose(); + } + + ChatSignalRService.MessageReceived -= OnMessageReceived; + ChatSignalRService.TypingNotificationReceived -= OnTypingNotificationReceived; + await ChatSignalRService.DisposeAsync(); + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Chats/DeleteChatDialog.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Chats/DeleteChatDialog.razor new file mode 100644 index 000000000..339f00394 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Chats/DeleteChatDialog.razor @@ -0,0 +1,19 @@ +@using MudBlazor + + + + @Message + + + Cancel + Delete + + + +@code { + [CascadingParameter] MudDialogInstance MudDialog { get; set; } + [Parameter] public string Message { get; set; } + + private void DeleteChat() => MudDialog.Close(DialogResult.Ok(true)); + private void Cancel() => MudDialog.Cancel(); +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Chats/NewChat.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Chats/NewChat.razor new file mode 100644 index 000000000..6b8cc15dd --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Chats/NewChat.razor @@ -0,0 +1,302 @@ +@page "/chats/new" +@using MiniSpace.Web.HttpClients +@using MiniSpace.Web.Areas.Friends +@using MiniSpace.Web.DTO +@inject IIdentityService IdentityService +@inject IFriendsService FriendsService +@inject ICommunicationService CommunicationService +@inject NavigationManager NavigationManager +@inject ISnackbar Snackbar +@using MudBlazor + + + + + +
+
+ + + @if (!pageInitialized) + { +
+ +
+ } + else if (friends != null && friends.Any()) + { + foreach (var friend in friends) + { +
+
+ Friend Image +
+
@friend.StudentDetails.FirstName @friend.StudentDetails.LastName
+

@friend.StudentDetails.Email

+
+
+ + + Start Chat + +
+
+
+ } + + @if (hasMorePages) + { + + + Load More + + } + } + else + { +

No friends to show. Start connecting now!

+ } +
+
+
+
+ + + +@code { + private List friends = new List(); + private string searchTerm; + private bool pageInitialized; + private int currentPage = 1; + private int pageSize = 10; + private int totalFriends; + private bool hasMorePages => friends.Count < totalFriends; + + + private List _items = new List + { + new BreadcrumbItem("Home", href: "/home", icon: Icons.Material.Filled.Home), + new BreadcrumbItem("Chats", href: "/chats", icon: Icons.Material.Filled.Chat), + new BreadcrumbItem("New Chat", href: "/chats/new", disabled: true, icon: Icons.Material.Filled.AddComment), + }; + + protected override async Task OnInitializedAsync() + { + await IdentityService.InitializeAuthenticationState(); + if (IdentityService.IsAuthenticated) + { + await LoadFriends(); + pageInitialized = true; + } + else + { + NavigationManager.NavigateTo("/login"); + } + } + + private string GetProfileImageUrl(string profileImageUrl) + { + return string.IsNullOrEmpty(profileImageUrl) ? "images/default_profile_image.webp" : profileImageUrl; + } + + private async Task StartChatWithFriend(Guid friendId, string friendName) + { + try + { + // Check if the chat already exists + var userId = IdentityService.GetCurrentUserId(); + var existingChat = await CommunicationService.FindExistingChatAsync(userId, friendId); + + if (existingChat != null) + { + // If chat exists, navigate to it + Console.WriteLine("Navigating to existing chat..."); + NavigationManager.NavigateTo($"/chats/{existingChat.Id}"); + } + else + { + // If no chat exists, create a new one + var command = new CreateChatCommand( + chatId: Guid.NewGuid(), + participantIds: new List { userId, friendId }, + chatName: $"Chat with {friendName}" + ); + + var response = await CommunicationService.CreateChatAsync(command); + + if (response.IsSuccessStatusCode) + { + Snackbar.Add($"You have started a chat with {friendName}.", Severity.Success); + NavigationManager.NavigateTo($"/chats/{response.Content}"); + } + else + { + Snackbar.Add("An error occurred while creating the chat.", Severity.Error); + } + } + } + catch (Exception ex) + { + Snackbar.Add($"An error occurred: {ex.Message}", Severity.Error); + } + } + + private void SearchFriends() + { + if (!string.IsNullOrWhiteSpace(searchTerm)) + { + searchTerm = searchTerm.Trim(); + friends = friends.Where(f => f.StudentDetails.FirstName.Contains(searchTerm, StringComparison.OrdinalIgnoreCase) || f.StudentDetails.LastName.Contains(searchTerm, StringComparison.OrdinalIgnoreCase)).ToList(); + } + else + { + ReloadFriends().Wait(); + } + StateHasChanged(); + } + + private async Task ReloadFriends() + { + currentPage = 1; + friends.Clear(); + await LoadFriends(); + } + + private async Task LoadFriends() + { + try + { + var studentId = IdentityService.GetCurrentUserId(); + var result = await FriendsService.GetAllFriendsAsync(studentId, currentPage, pageSize); + if (result != null) + { + friends.AddRange(result.Items); + totalFriends = result.TotalItems; + } + } + catch (Exception ex) + { + Snackbar.Add($"Failed to Load Friends: {ex.Message}", Severity.Error); + } + } + + private async Task LoadMoreFriends() + { + currentPage++; + await LoadFriends(); + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Events/MyEvents.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Events/MyEvents.razor index d0b3d89fe..36f114f46 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Pages/Events/MyEvents.razor +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Events/MyEvents.razor @@ -89,6 +89,6 @@ private void ViewEvent(Guid eventId) { - NavigationManager.NavigateTo($"/events/{eventId}"); + NavigationManager.NavigateTo($"/events/event/{eventId}"); } } diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Feeds/Home.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Feeds/Home.razor index d23b8e85a..e085838bd 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Pages/Feeds/Home.razor +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Feeds/Home.razor @@ -6,22 +6,37 @@ @inject NavigationManager NavigationManager @inject IIdentityService IdentityService @inject IPostsService PostsService +@inject ISnackbar Snackbar @using MudBlazor - - - - - + @if (!pageInitialized) + { +
+ + Loading, please wait... +
+ } + else + { + + + + + + - - - Discover What's New - @if (pageInitialized) - { - @if (posts.Any()) + + + Discover What's New + @if (postsLoadingFailed) + { + Failed to load posts. Please try again later. + } + else if (posts != null && posts.Any()) { @foreach (var post in posts) @@ -37,14 +52,14 @@ No activity found Please join an event first } - } - + - - - - - + + + + +
+ }
@@ -52,24 +67,43 @@ private IEnumerable posts; private Guid studentId; private bool pageInitialized = false; + private bool postsLoadingFailed = false; + + private List _items = new List + { + new BreadcrumbItem("Home", href: "/home", icon: Icons.Material.Filled.Home), + @* new BreadcrumbItem("Account settings", href: "/events/follow", disabled: true, icon: @Icons.Material.Filled.ManageAccounts), *@ + }; protected override async Task OnInitializedAsync() { if (IdentityService != null && IdentityService.IsAuthenticated) { studentId = IdentityService.GetCurrentUserId(); - var result = await PostsService.GetUserFeedAsync(studentId, 1, 8, "PublishDate", "desc"); - - if (result.IsSuccessStatusCode) + try + { + var result = await PostsService.GetUserFeedAsync(studentId, 1, 8, "PublishDate", "desc"); + + if (result.IsSuccessStatusCode) + { + posts = result.Content.Items; + } + else + { + posts = new List(); // Handle error gracefully + postsLoadingFailed = true; + Snackbar.Add($"Error loading posts: {result.ErrorMessage.Reason}", Severity.Error); + } + } + catch (Exception ex) { - posts = result.Content.Items; + postsLoadingFailed = true; + Snackbar.Add($"Exception occurred: {ex.Message}", Severity.Error); } - else + finally { - posts = new List(); // Handle error gracefully - Console.WriteLine($"Error: {result.ErrorMessage.Reason}"); + pageInitialized = true; } - pageInitialized = true; } else { diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Feeds/UserInformation.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Feeds/UserInformation.razor index ca5567418..843c2f28d 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Pages/Feeds/UserInformation.razor +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Feeds/UserInformation.razor @@ -1,125 +1,197 @@ @page "/user-information/{UserId:guid}" @using MiniSpace.Web.DTO @using MiniSpace.Web.Areas.Students +@using MiniSpace.Web.Areas.Events @inject IStudentsService StudentsService +@inject IEventsService EventsService @inject NavigationManager NavigationManager +@inject ISnackbar Snackbar @using MudBlazor - @if (student != null) - { - - - - + + + + + + @if (loadingProfile) + { + + } + else if (profileLoadFailed) + { + Failed to load user profile. Please try again later. + } + else if (student != null) + {
- +
@($"{student.FirstName} {student.LastName}") @student.Description - Edit Profile + Edit Profile + Public Profile
+ } +
+
+
+ + + @if (educationLoadFailed) + { + + Failed to load education details. + + } + else if (loadingEducation) + { + + + + } + else if (student?.Education != null && student.Education.Any()) + { + + + + + Education + + + @foreach (var education in student.Education) + { + @education.Degree at @education.InstitutionName + @education.StartDate?.ToString("MMMM yyyy") - @education.EndDate?.ToString("MMMM yyyy") + } + } - @if (!string.IsNullOrEmpty(student.ContactEmail)) - { - - - - - Contact Information - - - Contact Email: @student.ContactEmail - - - - } - - @if (student.Education != null && student.Education.Any()) - { - - - - - Education - - - @foreach (var education in student.Education) - { - Institution: @education.InstitutionName, Degree: @education.Degree, Period: @education.StartDate?.ToString("yyyy-MM-dd") - @education.EndDate?.ToString("yyyy-MM-dd") - } - - - - } - - @if (student.Work != null && student.Work.Any()) - { - - - - - Work Experience - - - @foreach (var work in student.Work) - { - Position: @work.Position, Company: @work.Company, Period: @work.StartDate?.ToString("yyyy-MM-dd") - @work.EndDate?.ToString("yyyy-MM-dd") - } - - - - } + + @if (workLoadFailed) + { + + Failed to load work experience. + + } + else if (loadingWork) + { + + + + } + else if (student?.Work != null && student.Work.Any()) + { + + + + + Work Experience + + + @foreach (var work in student.Work) + { + @work.Position at @work.Company + @work.StartDate?.ToString("MMMM yyyy") - @work.EndDate?.ToString("MMMM yyyy") + } + + + + } - @if ((student.Languages != null && student.Languages.Any()) || (student.Interests != null && student.Interests.Any())) - { - - - - - Skills and Interests - - - @if (student.Languages != null && student.Languages.Any()) - { - Languages: @string.Join(", ", student.Languages.Select(l => l.ToString())) - } - @if (student.Interests != null && student.Interests.Any()) - { - Interests: @string.Join(", ", student.Interests.Select(i => i.ToString())) - } - - - - } + + @if (languagesLoadFailed) + { + + Failed to load languages and interests. + + } + else if (loadingLanguages) + { + + + + } + else if ((student?.Languages != null && student.Languages.Any()) || (student?.Interests != null && student.Interests.Any())) + { + + + + + Skills and Interests + + + @if (student.Languages != null && student.Languages.Any()) + { + + Languages: + @foreach (var language in student.Languages) + { + @language + } + + } + @if (student.Interests != null && student.Interests.Any()) + { + + Interests: + @foreach (var interest in student.Interests) + { + @interest + } + + } + + + + } - @if ((student.InterestedInEvents != null && student.InterestedInEvents.Any()) || (student.SignedUpEvents != null && student.SignedUpEvents.Any())) - { - - - - - Events - - - @if (student.InterestedInEvents != null && student.InterestedInEvents.Any()) + + @if (eventsLoadFailed) + { + + Failed to load events. + + } + else if (loadingEvents) + { + + + + } + else if (InterestedInEventsDetails.Any() || SignedUpEventsDetails.Any()) + { + + + + + Events + + + @if (InterestedInEventsDetails.Any()) + { + Interested in: + @foreach (var eventDetail in InterestedInEventsDetails) { - Interested in Events: @string.Join(", ", student.InterestedInEvents.Select(e => e.ToString())) + @eventDetail.Name - @eventDetail.StartDate.ToString("MMMM dd, yyyy") } - @if (student.SignedUpEvents != null && student.SignedUpEvents.Any()) + } + @if (SignedUpEventsDetails.Any()) + { + Signed Up for: + @foreach (var eventDetail in SignedUpEventsDetails) { - Signed Up for Events: @string.Join(", ", student.SignedUpEvents.Select(e => e.ToString())) + @eventDetail.Name - @eventDetail.StartDate.ToString("MMMM dd, yyyy") } - - - - } -
- } + } +
+
+
+ } +
@code { @@ -127,15 +199,184 @@ public Guid UserId { get; set; } private StudentDto student; + private List InterestedInEventsDetails = new List(); + private List SignedUpEventsDetails = new List(); + + // Individual section loading and error state flags + private bool loadingProfile = true; + private bool profileLoadFailed = false; + + private bool loadingEducation = true; + private bool educationLoadFailed = false; + + private bool loadingWork = true; + private bool workLoadFailed = false; + + private bool loadingLanguages = true; + private bool languagesLoadFailed = false; + + private bool loadingEvents = true; + private bool eventsLoadFailed = false; + + private static readonly Random random = new Random(); protected override async Task OnInitializedAsync() { - student = await StudentsService.GetStudentAsync(UserId); + await LoadProfileAsync(); + + if (!profileLoadFailed) + { + await Task.WhenAll( + LoadEducationAsync(), + LoadWorkExperienceAsync(), + LoadLanguagesAndInterestsAsync(), + LoadEventsAsync() + ); + } + else + { + loadingEducation = false; + loadingWork = false; + loadingLanguages = false; + loadingEvents = false; + } + } + + private async Task LoadProfileAsync() + { + try + { + student = await StudentsService.GetStudentAsync(UserId); + } + catch (Exception ex) + { + profileLoadFailed = true; + Snackbar.Add($"Error loading user profile: {ex.Message}", Severity.Error); + } + finally + { + loadingProfile = false; + } + } + + private async Task LoadEducationAsync() + { + try + { + } + catch (Exception ex) + { + educationLoadFailed = true; + Snackbar.Add($"Error loading education details: {ex.Message}", Severity.Error); + } + finally + { + loadingEducation = false; + } + } + + private async Task LoadWorkExperienceAsync() + { + try + { + } + catch (Exception ex) + { + workLoadFailed = true; + Snackbar.Add($"Error loading work experience: {ex.Message}", Severity.Error); + } + finally + { + loadingWork = false; + } + } + + private async Task LoadLanguagesAndInterestsAsync() + { + try + { + } + catch (Exception ex) + { + languagesLoadFailed = true; + Snackbar.Add($"Error loading languages and interests: {ex.Message}", Severity.Error); + } + finally + { + loadingLanguages = false; + } + } + + private async Task LoadEventsAsync() + { + try + { + if (student.InterestedInEvents != null && student.InterestedInEvents.Any()) + { + foreach (var eventId in student.InterestedInEvents) + { + try + { + var eventDetail = await EventsService.GetEventAsync(eventId); + if (eventDetail != null) + { + InterestedInEventsDetails.Add(eventDetail); + } + else + { + Snackbar.Add($"Event details not found for event ID: {eventId}", Severity.Warning); + } + } + catch (Exception ex) + { + Snackbar.Add($"Error loading event details for event ID {eventId}: {ex.Message}", Severity.Error); + } + } + } + + if (student.SignedUpEvents != null && student.SignedUpEvents.Any()) + { + foreach (var eventId in student.SignedUpEvents) + { + try + { + var eventDetail = await EventsService.GetEventAsync(eventId); + if (eventDetail != null) + { + SignedUpEventsDetails.Add(eventDetail); + } + else + { + Snackbar.Add($"Signed up event details not found for event ID: {eventId}", Severity.Warning); + } + } + catch (Exception ex) + { + Snackbar.Add($"Error loading signed up event for event ID {eventId}: {ex.Message}", Severity.Error); + } + } + } + + if (!InterestedInEventsDetails.Any() && !SignedUpEventsDetails.Any()) + { + eventsLoadFailed = true; + Snackbar.Add("No events found for this user.", Severity.Warning); + } + } + catch (Exception ex) + { + eventsLoadFailed = true; + Snackbar.Add($"Error loading events: {ex.Message}", Severity.Error); + } + finally + { + loadingEvents = false; + } } private string GetProfileImage() { - var defaultImage = "path/to/default/image.png"; // Set path to your default image + var defaultImage = "images/default_profile_image.webp"; var validExtensions = new[] { ".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp" }; if (string.IsNullOrEmpty(student?.ProfileImageUrl) || !validExtensions.Contains(System.IO.Path.GetExtension(student.ProfileImageUrl)?.ToLower())) @@ -150,15 +391,29 @@ { NavigationManager.NavigateTo("/account"); } + + private void NavigateToPublicProfile() + { + NavigationManager.NavigateTo($"/user-details/{UserId}"); + } + + + + private int GetRandomFontSize() + { + return random.Next(14, 24); + } } + + + + .text-muted { + color: #7f8c8d; + } + + .skill-interest-text { + word-wrap: break-word; + white-space: normal; + display: flex; + flex-wrap: wrap; + } + + .skill-interest-item { + margin-right: 8px; + margin-bottom: 8px; + background-color: #f5f5f5; + padding: 2px 6px; + border-radius: 4px; + line-height: 1.2; /* Ensures good vertical spacing */ + display: inline-block; + max-width: 100%; /* Ensures item stays within container */ + } + + .info-card { + padding: 16px; + } + \ No newline at end of file diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Feeds/UserRelatedContent.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Feeds/UserRelatedContent.razor index bf8830f64..92edaefdd 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Pages/Feeds/UserRelatedContent.razor +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Feeds/UserRelatedContent.razor @@ -1,17 +1,41 @@ @using MiniSpace.Web.DTO -@using MiniSpace.Web.Areas.Students -@inject IStudentsService StudentsService +@using MiniSpace.Web.Areas.Events +@inject IEventsService EventsService @using MudBlazor - Recommendations - - @if (recommendations != null) + Event Recommendations + + @if (loadingRecommendations) { - @foreach (var recommendation in recommendations) - { - @recommendation - } + + } + else if (recommendations != null && recommendations.Any()) + { + + @foreach (var recommendation in recommendations) + { + + + + + + @recommendation.Name + @recommendation.StartDate.ToString("MMMM dd, yyyy") + @recommendation.Description + + + + } + + } + else + { + No recommendations available at the moment. } @@ -19,17 +43,25 @@ [Parameter] public Guid UserId { get; set; } - private List recommendations; + private List recommendations; + private bool loadingRecommendations = true; protected override async Task OnInitializedAsync() { - // Fetch recommendations based on the user ID recommendations = await FetchRecommendationsAsync(UserId); + loadingRecommendations = false; } - private Task> FetchRecommendationsAsync(Guid userId) + private async Task> FetchRecommendationsAsync(Guid userId) { - // Dummy implementation, replace with actual recommendation logic - return Task.FromResult(new List { "Event 1", "Event 2", "Event 3" }); + try + { + var result = await EventsService.GetUserEventsFeedAsync(userId, pageNumber: 1, pageSize: 10, sortBy: "PublishDate", direction: "asc"); + return result.Items.ToList(); + } + catch (Exception ex) + { + return new List(); + } } } diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Friends/Friends.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Friends/Friends.razor index ed9c53cff..746b2f90f 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Pages/Friends/Friends.razor +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Friends/Friends.razor @@ -194,7 +194,7 @@ private List _items = new List { - new BreadcrumbItem("Home", href: "/", icon: Icons.Material.Filled.Home), + new BreadcrumbItem("Home", href: "/home", icon: Icons.Material.Filled.Home), new BreadcrumbItem("Search", href: "/friends/search", icon: Icons.Material.Filled.PersonSearch), new BreadcrumbItem("Friends", href: "/friends", disabled: true, icon: Icons.Material.Filled.LibraryAddCheck), }; diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Friends/FriendsRequests.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Friends/FriendsRequests.razor index 702c18535..76067733f 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Pages/Friends/FriendsRequests.razor +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Friends/FriendsRequests.razor @@ -198,7 +198,7 @@ private List _items = new List { - new BreadcrumbItem("Home", href: "/", icon: Icons.Material.Filled.Home), + new BreadcrumbItem("Home", href: "/home", icon: Icons.Material.Filled.Home), new BreadcrumbItem("Search", href: "/friends/search", icon: Icons.Material.Filled.PersonSearch), new BreadcrumbItem("Friends", href: "/friends", icon: Icons.Material.Filled.LibraryAddCheck), new BreadcrumbItem("Requests", href: "/friends/requests", disabled: true, icon: Icons.Material.Filled.GroupAdd), diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Friends/FriendsSearch.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Friends/FriendsSearch.razor index 2b91ec1b1..602dbbaa3 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Pages/Friends/FriendsSearch.razor +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Friends/FriendsSearch.razor @@ -207,7 +207,7 @@ private List _items = new List { - new BreadcrumbItem("Home", href: "/", icon: Icons.Material.Filled.Home), + new BreadcrumbItem("Home", href: "/home", icon: Icons.Material.Filled.Home), new BreadcrumbItem("Search", href: "/friends/search", disabled: true, icon: Icons.Material.Filled.PersonSearch) }; diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Friends/SentRequests.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Friends/SentRequests.razor index 4d379cdc9..2a09a26fb 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Pages/Friends/SentRequests.razor +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Friends/SentRequests.razor @@ -194,7 +194,7 @@ private List _items = new List { - new BreadcrumbItem("Home", href: "/", icon: Icons.Material.Filled.Home), + new BreadcrumbItem("Home", href: "/home", icon: Icons.Material.Filled.Home), new BreadcrumbItem("Search", href: "/friends/search", icon: Icons.Material.Filled.PersonSearch), new BreadcrumbItem("Friends", href: "/friends", icon: Icons.Material.Filled.LibraryAddCheck), new BreadcrumbItem("Requests", href: "/friends/requests", icon: Icons.Material.Filled.GroupAdd), diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Friends/StudentDetails.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Friends/StudentDetails.razor deleted file mode 100644 index e1a9ef793..000000000 --- a/MiniSpace.Web/src/MiniSpace.Web/Pages/Friends/StudentDetails.razor +++ /dev/null @@ -1,283 +0,0 @@ -@page "/user-details/{Id:guid}" -@using MiniSpace.Web.Areas.Friends -@inject NavigationManager NavigationManager -@inject IFriendsService FriendsService -@using MiniSpace.Web.Areas.MediaFiles -@inject IMediaFilesService MediaFilesService -@using MiniSpace.Web.Models.Reports -@using MiniSpace.Web.Pages.Reports.Dialogs -@using DialogOptions = Radzen.DialogOptions -@using DialogService = Radzen.DialogService -@inject DialogService DialogService -@inject IIdentityService IdentityService -@using MiniSpace.Web.DTO -@using MudBlazor - - -
- - - @if (studentNotFound) - { -

Student profile not found!

-

Probably has been deleted!

- } - else if (student == null) - { - - } - else - { -
- -
- @if (ShouldDisplayProfileImage(student.UserSettings.ProfileImageVisibility)) - { - Profile Image - } -

@student.FirstName @student.LastName

-

@student.Email

-
-
-

Description: @student.Description

- @if (student.Education != null && student.Education.Any()) - { -

Education:

- @foreach (var education in student.Education) - { -

@education.InstitutionName - @education.Degree (@education.StartDate?.ToLocalTime().ToString("yyyy-MM-dd") to @education.EndDate?.ToLocalTime().ToString("yyyy-MM-dd"))

- } - } - @if (student.Work != null && student.Work.Any()) - { -

Work:

- @foreach (var work in student.Work) - { -

@work.Company - @work.Position (@work.StartDate?.ToLocalTime().ToString("yyyy-MM-dd") to @work.EndDate?.ToLocalTime().ToString("yyyy-MM-dd"))

- } - } - @if (student.Languages != null && student.Languages.Any()) - { -

Languages: @string.Join(", ", student.Languages)

- } - @if (student.Interests != null && student.Interests.Any()) - { -

Interests: @string.Join(", ", student.Interests)

- } -

Date of Birth: @student.DateOfBirth?.ToLocalTime().ToString("yyyy-MM-dd")

-

State: @student.State

-

Joined: @student.CreatedAt.ToLocalTime().ToString("yyyy-MM-dd")

-
- @if (IdentityService.IsAuthenticated) - { - - Report profile - - } -
- - @if (ShouldDisplayGallery(student.UserSettings.GalleryVisibility)) - { -

Gallery

- - Gallery Images - - @if (student.GalleryOfImageUrls != null && student.GalleryOfImageUrls.Any(img => IsValidImageUrl(img.ImageUrl))) - { - @foreach (var galleryImage in student.GalleryOfImageUrls.Where(img => IsValidImageUrl(img.ImageUrl))) - { - - - - - - - - } - } - else - { - No gallery images available. - } - - - } - } -
-
- - - -@code { - [Parameter] public Guid Id { get; set; } - private StudentDto student; - private bool studentNotFound; - private List _items; - - protected override async Task OnInitializedAsync() - { - _items = new List - { - new BreadcrumbItem("Home", href: "/", icon: Icons.Material.Filled.Home), - new BreadcrumbItem("Search", href: "/friends/search", icon: Icons.Material.Filled.PersonSearch), - new BreadcrumbItem("Friends", href: "/friends", icon: Icons.Material.Filled.LibraryAddCheck), - new BreadcrumbItem("Requests", href: "/friends/requests", icon: Icons.Material.Filled.GroupAdd), - new BreadcrumbItem("Sent Requests", href: "/friends/sent-requests", icon: Icons.Material.Filled.PersonAddAlt1), - new BreadcrumbItem("Student details", href: $"/user-details/{Id}", disabled: true, icon: Icons.Material.Filled.Person) - }; - - await IdentityService.InitializeAuthenticationState(); - if (IdentityService.IsAuthenticated) - { - student = await FriendsService.GetStudentAsync(Id); - if (student == null) - { - studentNotFound = true; - return; - } - var studentJson = System.Text.Json.JsonSerializer.Serialize(student, new System.Text.Json.JsonSerializerOptions - { - WriteIndented = true // This will format the JSON output with indents for readability - }); - Console.WriteLine("Student Object JSON:"); - Console.WriteLine(studentJson); - } - else - { - NavigationManager.NavigateTo("/login"); - } - } - - private string GetProfileImageUrl(string profileImageUrl) - { - return string.IsNullOrEmpty(profileImageUrl) ? "images/default_profile_image.webp" : profileImageUrl; - } - - private bool IsValidImageUrl(string url) - { - if (string.IsNullOrEmpty(url)) - return false; - - string[] validExtensions = { ".jpg", ".jpeg", ".png", ".gif", ".webp" }; - string extension = System.IO.Path.GetExtension(url)?.ToLower(); - return validExtensions.Contains(extension); - } - - private async Task ReportStudentProfile(StudentDto studentDto) - { - var createReportModel = new CreateReportModel - { - IssuerId = IdentityService.GetCurrentUserId(), - TargetId = studentDto.Id, - TargetOwnerId = studentDto.Id, - ContextType = "StudentProfile" - }; - - await DialogService.OpenAsync("Report profile of the student:", - new Dictionary() { { "CreateReportModel", createReportModel } }, - new DialogOptions() - { - Width = "700px", Height = "350px", Resizable = true, Draggable = true, - AutoFocusFirstElement = false - }); - } - - private bool ShouldDisplayGallery(Visibility galleryVisibility) - { - return galleryVisibility == Visibility.Everyone || (galleryVisibility == Visibility.Connections && IsFriend()); - } - - private bool ShouldDisplayProfileImage(Visibility profileImageVisibility) - { - return profileImageVisibility == Visibility.Everyone || (profileImageVisibility == Visibility.Connections && IsFriend()); - } - - private bool ShouldDisplayBannerImage(Visibility bannerImageVisibility) - { - return bannerImageVisibility == Visibility.Everyone || (bannerImageVisibility == Visibility.Connections && IsFriend()); - } - - private bool IsFriend() - { - // Implement logic to determine if the current user is a friend - // For now, returning true for demonstration purposes - return true; - } -} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Friends/UserDetails.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Friends/UserDetails.razor new file mode 100644 index 000000000..c48d298ba --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Friends/UserDetails.razor @@ -0,0 +1,681 @@ +@page "/user-details/{Id:guid}" +@using MiniSpace.Web.Areas.Friends +@using MiniSpace.Web.Areas.Organizations +@using MiniSpace.Web.Areas.Students +@inject NavigationManager NavigationManager +@inject IFriendsService FriendsService +@inject IEventsService EventsService +@inject IOrganizationsService OrganizationsService +@inject ISnackbar Snackbar +@inject IIdentityService IdentityService +@inject IStudentsService StudentsService +@using MiniSpace.Web.DTO +@using MudBlazor + + + + @if (studentNotFound) + { + + Student profile not found! + It may have been deleted or is inaccessible. + + } + else if (student == null) + { + + } + else + { + + + + + + + @student.FirstName @student.LastName + + @if (!string.IsNullOrWhiteSpace(student.Description)) + { + @student.Description + } + + + + @if (student.DateOfBirth.HasValue) + { + Date of Birth: @student.DateOfBirth?.ToLocalTime().ToString("yyyy-MM-dd") + } + + @if (!string.IsNullOrWhiteSpace(student.City) || !string.IsNullOrWhiteSpace(student.Country)) + { + Location: @student.City, @student.Country + } + + @if (!string.IsNullOrWhiteSpace(student.State)) + { + State: @student.State + } + + + Joined: @student.CreatedAt.ToLocalTime().ToString("yyyy-MM-dd") + + + + + + + @if (ShouldDisplayGallery(student.UserSettings.GalleryVisibility) && student.GalleryOfImageUrls?.Any(img => IsValidImageUrl(img.ImageUrl)) == true) + { + + + Gallery + + + + @foreach (var galleryImage in student.GalleryOfImageUrls.Where(img => IsValidImageUrl(img.ImageUrl))) + { + + + + + + } + + + + } + + + + @if (followers?.Any() == true) + { + + + Followers + + + + @foreach (var follower in followers) + { + + + + + @follower.StudentDetails.FirstName @follower.StudentDetails.LastName + + } + + + + } + + + + @if (following?.Any() == true) + { + + + Following + + + + @foreach (var follow in following) + { + + + + + @follow.StudentDetails.FirstName @follow.StudentDetails.LastName + + } + + + + } + + + + @if (friends?.Any() == true) + { + + + Friends + + + + @foreach (var friend in friends) + { + + + + + @friend.StudentDetails.FirstName @friend.StudentDetails.LastName + + } + + + + } + + + + + + @if (sentFriendRequests?.Any() == true) + { + + + Sent Friend Requests + + + + @foreach (var request in sentFriendRequests) + { + + @request.InviteeName + + } + + + + } + + + @if (incomingFriendRequests?.Any() == true) + { + + + Incoming Friend Requests + + + + @foreach (var request in incomingFriendRequests) + { + + @request.InviterName + + } + + + + } + + + + + + @if (InterestedInEventsDetails?.Any() == true) + { + + + Interested Events + + + + @foreach (var eventDetail in InterestedInEventsDetails) + { + + + + + + + @eventDetail.Name - @eventDetail.StartDate.ToString("MMMM dd, yyyy") + + + + } + + + + } + + + @if (userEvents?.Any() == true) + { + + + Signed Up Events + + + + @foreach (var eventDto in userEvents) + { + + + + + + + @eventDto.Name - @eventDto.StartDate.ToString("MMMM dd, yyyy") + + + + } + + + + } + + + + + + @if (userOrganizations?.Any() == true) + { + + + User's Organizations + + + + @foreach (var organization in userOrganizations) + { + + + + + + + + @organization.Name + @organization.Description + @organization.UserCount users + + + + } + + + + } + + + + @if (followedOrganizations?.Any() == true) + { + + + Organizations Followed + + + + @foreach (var organization in followedOrganizations) + { + + + + + + + + @organization.OrganizationDetails.Name + @organization.OrganizationDetails.Description + @organization.OrganizationDetails.Users.Count() users + + + + } + + + + } + + + } + + + + + +@code { + [Parameter] public Guid Id { get; set; } + private StudentDto student; + private Guid currentUserId; + private bool studentNotFound; + private List userEvents = new List(); + private List InterestedInEventsDetails = new List(); + private List userOrganizations = new List(); + private List followedOrganizations = new List(); + private List friends; + private List followers; + private List following; + private List sentFriendRequests; + private List incomingFriendRequests; + + protected override async Task OnInitializedAsync() + { + await IdentityService.InitializeAuthenticationState(); + if (IdentityService.IsAuthenticated) + { + currentUserId = IdentityService.GetCurrentUserId(); + student = await FriendsService.GetStudentAsync(Id); + if (student == null) + { + studentNotFound = true; + } + else + { + await LoadUserData(); + } + } + else + { + NavigationManager.NavigateTo("/login"); + } + } + + private async Task LoadUserData() + { + var pagedEvents = await EventsService.GetUserEventsAsync(Id, 1, 10, "signed_up"); + userEvents = pagedEvents?.Items?.ToList() ?? new List(); + + friends = (await FriendsService.GetAllFriendsAsync(Id, 1, 10))?.Items?.ToList() ?? new List(); + followers = (await FriendsService.GetPagedFollowersAsync(Id, 1, 10))?.Items?.ToList() ?? new List(); + following = (await FriendsService.GetPagedFollowingAsync(Id, 1, 10))?.Items?.ToList() ?? new List(); + + var sentRequestsPaged = await FriendsService.GetSentFriendRequestsAsync(1, 10); + sentFriendRequests = sentRequestsPaged?.Items?.ToList() ?? new List(); + + var incomingRequestsPaged = await FriendsService.GetIncomingFriendRequestsAsync(1, 10); + incomingFriendRequests = incomingRequestsPaged?.Items?.ToList() ?? new List(); + + await LoadInterestedAndSignedUpEventsAsync(); + await LoadUserOrganizationsAsync(); + await LoadFollowedOrganizationsAsync(); + } + + private async Task LoadInterestedAndSignedUpEventsAsync() + { + try + { + // Load interested events + if (student.InterestedInEvents != null && student.InterestedInEvents.Any()) + { + foreach (var eventId in student.InterestedInEvents) + { + var eventDetail = await EventsService.GetEventAsync(eventId); + if (eventDetail != null) + { + InterestedInEventsDetails.Add(eventDetail); + } + } + } + + // Load signed-up events + if (student.SignedUpEvents != null && student.SignedUpEvents.Any()) + { + foreach (var eventId in student.SignedUpEvents) + { + var eventDetail = await EventsService.GetEventAsync(eventId); + if (eventDetail != null) + { + userEvents.Add(eventDetail); + } + } + } + } + catch (Exception ex) + { + Snackbar.Add($"Error loading events: {ex.Message}", Severity.Error); + } + } + + private async Task LoadUserOrganizationsAsync() + { + try + { + var pagedOrganizations = await OrganizationsService.GetPaginatedUserOrganizationsAsync(Id, 1, 10); + userOrganizations = pagedOrganizations?.Items?.ToList() ?? new List(); + } + catch (Exception ex) + { + Snackbar.Add($"Error loading user's organizations: {ex.Message}", Severity.Error); + } + } + + private async Task LoadFollowedOrganizationsAsync() + { + try + { + var organizations = await OrganizationsService.GetUserFollowedOrganizationsAsync(Id); + followedOrganizations = organizations?.ToList() ?? new List(); + } + catch (Exception ex) + { + Snackbar.Add($"Error loading followed organizations: {ex.Message}", Severity.Error); + } + } + + private string GetProfileImageUrl(string profileImageUrl) + { + return string.IsNullOrEmpty(profileImageUrl) ? "/images/default_profile_image.webp" : profileImageUrl; + } + + private string GetBannerImageUrl(string bannerImageUrl) + { + return string.IsNullOrEmpty(bannerImageUrl) ? "/images/default_banner_image.png" : bannerImageUrl; + } + + private bool IsValidImageUrl(string url) + { + if (string.IsNullOrEmpty(url)) + return false; + + string[] validExtensions = { ".jpg", ".jpeg", ".png", ".gif", ".webp" }; + string extension = System.IO.Path.GetExtension(url)?.ToLower(); + return validExtensions.Contains(extension); + } + + private bool IsFriend(FriendDto friend) + { + return friends.Any(f => f.FriendId == friend.FriendId); + } + + private bool ShouldDisplayGallery(Visibility galleryVisibility) + { + return galleryVisibility == Visibility.Everyone || (galleryVisibility == Visibility.Connections && IsFriend(null)); + } + + private async Task AddFriend() + { + if (student != null) + { + await FriendsService.AddFriendAsync(student.Id); + Snackbar.Add("Friend request sent.", Severity.Success); + } + } + + private async Task BlockUser() + { + if (student != null) + { + await StudentsService.BlockUserAsync(currentUserId, student.Id); + Snackbar.Add("User blocked.", Severity.Warning); + } + } + + private void ReportUser() + { + // Implement the logic to report the user here + Snackbar.Add("User reported.", Severity.Info); + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Index.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Index.razor index 636f670fd..02c3c036e 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Pages/Index.razor +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Index.razor @@ -29,12 +29,6 @@ } - @* - Mini Space - @titles[activeIndex] - @descriptions[activeIndex] - Get Started - *@

Welcome to Mini Space

@titles[activeIndex]

@@ -131,4 +125,6 @@ "Connect with friends and family, share your experiences.", "Share your adventures and stories with a global audience." }; + + } diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Notifications/AllNotifications.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Notifications/AllNotifications.razor index 9baa76747..ccdf98437 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Pages/Notifications/AllNotifications.razor +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Notifications/AllNotifications.razor @@ -3,80 +3,114 @@ @inject INotificationsService NotificationsService @inject NavigationManager NavigationManager @using MiniSpace.Web.DTO.Notifications -@using Radzen -@using System.Linq @inject IIdentityService IdentityService @using MudBlazor +@using System.Collections.Generic +@using System.Threading.Tasks - -@code { - private List _items = new List - { - new BreadcrumbItem("Home", href: "/", icon: Icons.Material.Filled.Home), - new BreadcrumbItem("Notifications", href: "/notifications/all", icon: Icons.Material.Filled.Notifications) - }; -} + -
-

All Notifications

-
- -@if (notifications == null) -{ -
- - - + @code { + private List _items = new List + { + new BreadcrumbItem("Home", href: "/home", icon: Icons.Material.Filled.Home), + new BreadcrumbItem("Notifications", href: "/notifications/all", icon: Icons.Material.Filled.Notifications) + }; + } + +
+

All Notifications

-} -else if (notifications.Any()) -{ - - - - - - - - - - - - - - -} -else -{ -

No notifications found.

-} + @if (notifications == null) + { +
+ +
+ } + else if (notifications.Any()) + { + + @foreach (var notification in notifications) + { + + + + + @notification.Message + @notification.CreatedAt.ToString("MMMM dd, yyyy") + + + + + + @if (notification.Status == "Unread") + { + + + Mark as Read + + } + else + { + + + Mark as Unread + + } + + + + + Delete + + + + + } + + } + else + { +

No notifications found.

+ } + + + @code { - private List notifications; + private List notifications = new(); private int currentPage = 1; private int pageSize = 10; private int totalNotifications; @@ -85,8 +119,8 @@ else { await IdentityService.InitializeAuthenticationState(); if (IdentityService.IsAuthenticated) - { - await LoadNotifications(new LoadDataArgs { Skip = 0, Top = pageSize, OrderBy = "createdAt desc" }); + { + await LoadNotifications(); } else { @@ -94,16 +128,8 @@ else } } - - private async Task LoadNotifications(LoadDataArgs args) + private async Task LoadNotifications() { - - var skip = args.Skip ?? 0; - var top = args.Top ?? pageSize; - - currentPage = (skip / top) + 1; - pageSize = top; - var userId = IdentityService.GetCurrentUserId(); var response = await NotificationsService.GetNotificationsByUserAsync(userId, page: currentPage, pageSize: pageSize, sortOrder: "desc", status: null); @@ -111,45 +137,25 @@ else { notifications = response.Results; totalNotifications = response.Total; - StateHasChanged(); + StateHasChanged(); } } - - private async Task UpdateNotificationStatus(Guid userId, Guid notificationId, bool isUnread) + private async Task UpdateNotificationStatus(Guid userId, Guid notificationId, string newStatus) { - string newStatus = isUnread ? "Unread" : "Read"; await NotificationsService.UpdateNotificationStatusAsync(userId, notificationId, newStatus); - - // Refresh the notifications to reflect the change - var notification = notifications.FirstOrDefault(n => n.NotificationId == notificationId); + var notification = notifications.Find(n => n.NotificationId == notificationId); if (notification != null) { notification.Status = newStatus; - await LoadNotifications(new LoadDataArgs { Skip = (currentPage - 1) * pageSize, Top = pageSize, OrderBy = "createdAt desc" }); - StateHasChanged(); // - await OnInitializedAsync(); - } - else - { - StateHasChanged(); // This will force the UI to update if for some reason the notification is not found - await OnInitializedAsync(); + StateHasChanged(); } } - - - - private async Task DeleteNotification(Guid userId, Guid notificationId) + private async Task DeleteNotification(Guid userId, Guid notificationId) { await NotificationsService.DeleteNotificationAsync(userId, notificationId); - // Use FindIndex to locate the specific notification and remove it if found - int index = notifications.FindIndex(n => n.NotificationId == notificationId); - if (index != -1) - { - notifications.RemoveAt(index); - StateHasChanged(); // Update UI to reflect the removal - } + notifications.RemoveAll(n => n.NotificationId == notificationId); + StateHasChanged(); } - } diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Notifications/HistoryNotifications.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Notifications/HistoryNotifications.razor index 33cbb9639..0f61f1977 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Pages/Notifications/HistoryNotifications.razor +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Notifications/HistoryNotifications.razor @@ -3,88 +3,124 @@ @inject INotificationsService NotificationsService @inject NavigationManager NavigationManager @using MiniSpace.Web.DTO.Notifications -@using Radzen -@using System.Linq -@inject IIdentityService IdentityService @using MudBlazor +@inject IIdentityService IdentityService +@using System.Collections.Generic +@using System.Threading.Tasks - -@code { - private List _items = new List - { - new BreadcrumbItem("Home", href: "/", icon: Icons.Material.Filled.Home), - new BreadcrumbItem("Notifications", href: "/notifications/all", icon: Icons.Material.Filled.Notifications), - new BreadcrumbItem("New Notifications", href: "/notifications/new", icon: Icons.Material.Filled.NotificationsActive), - new BreadcrumbItem("Notifications History", href: "/notifications/history", disabled: true, icon: Icons.Material.Filled.NotificationsPaused) - }; -} + -
-

Notifications History

-
- -@if (notifications == null) -{ -
- - - + @code { + private List _items = new List + { + new BreadcrumbItem("Home", href: "/home", icon: Icons.Material.Filled.Home), + new BreadcrumbItem("Notifications", href: "/notifications/all", icon: Icons.Material.Filled.Notifications), + new BreadcrumbItem("New Notifications", href: "/notifications/new", icon: Icons.Material.Filled.NotificationsActive), + new BreadcrumbItem("Notifications History", href: "/notifications/history", disabled: true, icon: Icons.Material.Filled.NotificationsPaused) + }; + } + +
+

Notifications History

-} -else if (notifications.Any()) -{ - - - - - - - - - - - - - - -} -else -{ -

No notifications found.

-} + @if (notifications == null) + { +
+ +
+ } + else if (notifications.Any()) + { + + @foreach (var notification in notifications) + { + + + + + @notification.Message + @notification.CreatedAt.ToString("MMMM dd, yyyy") + + + + + @if (notification.Status == "Unread") + { + + + Mark as Read + + } + else + { + + + Mark as Unread + + } + + + + Delete + + + + + } + + } + else + { +

No notifications found.

+ } + + + @code { - private List notifications; + private List notifications = new(); private int currentPage = 1; private int pageSize = 10; private int totalNotifications; protected override async Task OnInitializedAsync() { + await IdentityService.InitializeAuthenticationState(); if (IdentityService.IsAuthenticated) { - await LoadNotifications(new LoadDataArgs { Skip = 0, Top = pageSize, OrderBy = "createdAt desc" }); + await LoadNotifications(); } else { @@ -92,16 +128,8 @@ else } } - - private async Task LoadNotifications(LoadDataArgs args) + private async Task LoadNotifications() { - - var skip = args.Skip ?? 0; - var top = args.Top ?? pageSize; - - currentPage = (skip / top) + 1; - pageSize = top; - var userId = IdentityService.GetCurrentUserId(); var response = await NotificationsService.GetNotificationsByUserAsync(userId, page: currentPage, pageSize: pageSize, sortOrder: "desc", status: "Read"); @@ -109,16 +137,19 @@ else { notifications = response.Results; totalNotifications = response.Total; - StateHasChanged(); + StateHasChanged(); } } - - private async Task UpdateNotificationStatus(Guid userId, Guid notificationId, string newStatus) + private async Task UpdateNotificationStatus(Guid userId, Guid notificationId, string newStatus) { await NotificationsService.UpdateNotificationStatusAsync(userId, notificationId, newStatus); - notifications.Find(n => n.NotificationId == notificationId).Status = newStatus; - StateHasChanged(); + var notification = notifications.Find(n => n.NotificationId == notificationId); + if (notification != null) + { + notification.Status = newStatus; + StateHasChanged(); + } } private async Task DeleteNotification(Guid userId, Guid notificationId) diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Notifications/NewNotifications.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Notifications/NewNotifications.razor index adf3e8c14..dd11b8748 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Pages/Notifications/NewNotifications.razor +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Notifications/NewNotifications.razor @@ -3,77 +3,113 @@ @inject INotificationsService NotificationsService @inject NavigationManager NavigationManager @using MiniSpace.Web.DTO.Notifications -@using Radzen -@using System.Linq @inject IIdentityService IdentityService @using MudBlazor +@using System.Collections.Generic +@using System.Threading.Tasks - -@code { - private List _items = new List - { - new BreadcrumbItem("Home", href: "/", icon: Icons.Material.Filled.Home), - new BreadcrumbItem("Notifications", href: "/notifications/all", icon: Icons.Material.Filled.Notifications), - new BreadcrumbItem("New Notifications", href: "/notifications/new", disabled: true, icon: Icons.Material.Filled.NotificationsActive) - }; -} + + + @code { + private List _items = new List + { + new BreadcrumbItem("Home", href: "/home", icon: Icons.Material.Filled.Home), + new BreadcrumbItem("Notifications", href: "/notifications/all", icon: Icons.Material.Filled.Notifications), + new BreadcrumbItem("New Notifications", href: "/notifications/new", disabled: true, icon: Icons.Material.Filled.NotificationsActive) + }; + } -
-

Recent Notifications

-
- -@if (notifications == null) -{ -
- - - +
+

Recent Notifications

-} -else if (notifications.Any()) -{ - - - - - - - - - - - - - - -} -else -{ -

No notifications found.

-} + + @if (notifications == null) + { +
+ +
+ } + else if (notifications.Any()) + { + + @foreach (var notification in notifications) + { + + + + + @notification.Message + @notification.CreatedAt.ToString("MMMM dd, yyyy") + + + + + @if (notification.Status == "Unread") + { + + + Mark as Read + + } + else + { + + + Mark as Unread + + } + + + + Delete + + + + + } + + } + else + { +

No notifications found.

+ } + + + @code { - private List notifications; + private List notifications = new(); private int currentPage = 1; private int pageSize = 10; private int totalNotifications; @@ -82,8 +118,8 @@ else { await IdentityService.InitializeAuthenticationState(); if (IdentityService.IsAuthenticated) - { - await LoadNotifications(new LoadDataArgs { Skip = 0, Top = pageSize, OrderBy = "createdAt desc" }); + { + await LoadNotifications(); } else { @@ -91,15 +127,8 @@ else } } - - private async Task LoadNotifications(LoadDataArgs args) + private async Task LoadNotifications() { - var skip = args.Skip ?? 0; - var top = args.Top ?? pageSize; - - currentPage = (skip / top) + 1; - pageSize = top; - var userId = IdentityService.GetCurrentUserId(); var response = await NotificationsService.GetNotificationsByUserAsync(userId, page: currentPage, pageSize: pageSize, sortOrder: "desc", status: "Unread"); @@ -107,16 +136,19 @@ else { notifications = response.Results; totalNotifications = response.Total; - StateHasChanged(); + StateHasChanged(); } } - - private async Task UpdateNotificationStatus(Guid userId, Guid notificationId, string newStatus) + private async Task UpdateNotificationStatus(Guid userId, Guid notificationId, string newStatus) { await NotificationsService.UpdateNotificationStatusAsync(userId, notificationId, newStatus); - notifications.Find(n => n.NotificationId == notificationId).Status = newStatus; - StateHasChanged(); + var notification = notifications.Find(n => n.NotificationId == notificationId); + if (notification != null) + { + notification.Status = newStatus; + StateHasChanged(); + } } private async Task DeleteNotification(Guid userId, Guid notificationId) diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Notifications/Notification.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Notifications/Notification.razor index f137b2d25..f90a0511e 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Pages/Notifications/Notification.razor +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Notifications/Notification.razor @@ -14,7 +14,7 @@ @code { private List _items = new List { - new BreadcrumbItem("Home", href: "/", icon: Icons.Material.Filled.Home), + new BreadcrumbItem("Home", href: "/home", icon: Icons.Material.Filled.Home), new BreadcrumbItem("Notifications", href: "/notifications/all", icon: Icons.Material.Filled.Notifications), new BreadcrumbItem("New Notifications", href: "/notifications/new", icon: Icons.Material.Filled.NotificationsActive), new BreadcrumbItem("Notifications History", href: "/notifications/history", icon: Icons.Material.Filled.NotificationsPaused), diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Organizations/EventsComponent.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Organizations/EventsComponent.razor index 282829e43..33ee6465f 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Pages/Organizations/EventsComponent.razor +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Organizations/EventsComponent.razor @@ -1,3 +1,4 @@ +@page "/organizations/{OrganizationId}/events" @inject IEventsService EventsService @inject NavigationManager NavigationManager @inject IIdentityService IdentityService @@ -101,7 +102,7 @@ } catch (Exception ex) { - Console.Error.WriteLine(ex); + Console.Error.WriteLine(ex); // Log to console for debugging Snackbar.Add($"Failed to load events: {ex.Message}", Severity.Error); } finally @@ -112,18 +113,34 @@ private async Task LoadEvents() { - var searchCommand = new SearchEvents + try { - OrganizationId = OrganizationId, - Pageable = new PageableDto + var searchCommand = new SearchEvents { - Page = 1, - Size = 50 - } - }; + OrganizationId = OrganizationId, + Pageable = new PageableDto + { + Page = 1, + Size = 50 + } + }; + + var result = await EventsService.SearchEventsAsync(searchCommand); + + // Debugging: Log result to ensure data is returned + Console.WriteLine($"Events fetched: {result?.Items.Count()}"); - var result = await EventsService.SearchEventsAsync(searchCommand); - events = result?.Items.ToList() ?? new List(); + events = result?.Items.ToList() ?? new List(); + } + catch (Exception ex) + { + Console.Error.WriteLine(ex); // Log to console for debugging + Snackbar.Add($"Failed to load events: {ex.Message}", Severity.Error); + } + finally + { + isLoading = false; + } } private bool CheckIfUserIsAdmin(OrganizationDetailsDto organization) diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Organizations/OrganizationDetails.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Organizations/OrganizationDetails.razor index 461581195..022148f0c 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Pages/Organizations/OrganizationDetails.razor +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Organizations/OrganizationDetails.razor @@ -65,8 +65,9 @@ Posts Events Members - Suborganizations - Gallery + Requests + Suborganizations + Gallery @@ -89,11 +90,15 @@ { } - else if (selectedTabIndex == 5) + else if (selectedTabIndex == 5) { - + } else if (selectedTabIndex == 6) + { + + } + else if (selectedTabIndex == 7) { } diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Organizations/OrganizationMembersComponent.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Organizations/OrganizationMembersComponent.razor index 3bb3e7b22..6f6f90a41 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Pages/Organizations/OrganizationMembersComponent.razor +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Organizations/OrganizationMembersComponent.razor @@ -30,7 +30,6 @@ @member.FirstName @member.LastName - @* @member.Role?.Name *@ diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Organizations/OrganizationRequestsComponent.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Organizations/OrganizationRequestsComponent.razor new file mode 100644 index 000000000..11b768e25 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Organizations/OrganizationRequestsComponent.razor @@ -0,0 +1,164 @@ +@page "/organizations/{OrganizationId:guid}/requests" +@using MudBlazor +@inject ISnackbar Snackbar +@inject IOrganizationsService OrganizationsService +@inject IStudentsService StudentsService + +@code { + [Parameter] + public Guid OrganizationId { get; set; } + + private List requests = new List(); + private Dictionary users = new Dictionary(); + private bool isLoading = true; + private int page = 1; + private int pageSize = 10; + private int totalItems; + + protected override async Task OnInitializedAsync() + { + await LoadRequests(); + } + + private async Task LoadRequests() + { + isLoading = true; + try + { + var result = await OrganizationsService.GetOrganizationRequestsAsync(OrganizationId, page, pageSize); + if (result != null) + { + requests = result.Items.ToList(); + totalItems = result.TotalItems; + + foreach (var request in requests) + { + if (!users.ContainsKey(request.UserId)) + { + var user = await StudentsService.GetStudentAsync(request.UserId); + if (user != null) + { + users[request.UserId] = user; + } + } + } + } + } + catch (Exception ex) + { + Console.Error.WriteLine(ex); + Snackbar.Add("Failed to load organization requests.", Severity.Error); + } + finally + { + isLoading = false; + } + } + + private async Task AcceptRequest(Guid requestId) + { + try + { + await OrganizationsService.AcceptFollowRequestAsync(OrganizationId, requestId); + Snackbar.Add("Request accepted successfully.", Severity.Success); + await LoadRequests(); + } + catch (Exception ex) + { + Console.Error.WriteLine(ex); + Snackbar.Add("Failed to accept request.", Severity.Error); + } + } + + private async Task RejectRequest(Guid requestId) + { + try + { + await OrganizationsService.RejectFollowRequestAsync(OrganizationId, requestId, "Request rejected by admin."); + Snackbar.Add("Request rejected successfully.", Severity.Success); + await LoadRequests(); + } + catch (Exception ex) + { + Console.Error.WriteLine(ex); + Snackbar.Add("Failed to reject request.", Severity.Error); + } + } + + private void OnPageChanged(int newPage) + { + page = newPage + 1; // Adjust to handle MudTablePager's zero-based index + _ = LoadRequests(); + } + + private string GetUserAvatar(Guid userId) + { + return users.ContainsKey(userId) && !string.IsNullOrEmpty(users[userId]?.ProfileImageUrl) + ? users[userId].ProfileImageUrl + : "/images/default_profile_image.png"; + } +} + + + @if (isLoading) + { + + } + else if (requests == null || !requests.Any()) + { + No requests found for this organization. + } + else + { + + @foreach (var request in requests) + { + + + + + + + + + + + + @users[request.UserId]?.FirstName @users[request.UserId]?.LastName + + + Requested on @request.RequestDate.ToString("g") + + + @request.State + + + Reason: @request.Reason + + + + + + + + Accept + + + Reject + + + + + + + + } + + + + } + diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Organizations/OrganizationsFollowing.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Organizations/OrganizationsFollowing.razor index ef0c2cde8..6be2f7eea 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Pages/Organizations/OrganizationsFollowing.razor +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Organizations/OrganizationsFollowing.razor @@ -7,40 +7,50 @@ @using System.Threading.Tasks - - Organizations You're Following + + + Organizations You're Following - - @if (_organizations != null && _organizations.Any()) - { - @foreach (var organization in _organizations) + + + + @if (_organizations != null && _organizations.Any()) { - - - - - @organization.OrganizationDetails.Name - @organization.OrganizationDetails.Description - Users: @organization.Users.Count() - - - - View - - - - + @foreach (var organization in _organizations) + { + + + + + + @organization.OrganizationDetails.Name + @organization.OrganizationDetails.Description + Users: @organization.Users.Count() + + + + View + + + + + } } - } - else - { - You're not following any organizations yet. - } - - + else if (_organizations == null) + { + Loading followed organizations... + } + else + { + No followed organizations found. + } + + + @code { + private string _searchQuery = string.Empty; private IEnumerable _organizations; private bool _isLoading = true; @@ -54,7 +64,7 @@ if (IdentityService.IsAuthenticated) { - _organizations = await OrganizationsService.GetUserFollowedOrganizationsAsync(IdentityService.UserDto.Id); + await SearchOrganizations(); } else { @@ -71,8 +81,48 @@ } } + private async Task SearchOrganizations() + { + try + { + _isLoading = true; + var followedOrganizations = await OrganizationsService.GetUserFollowedOrganizationsAsync(IdentityService.UserDto.Id); + + if (!string.IsNullOrEmpty(_searchQuery)) + { + _organizations = followedOrganizations.Where(o => o.OrganizationDetails.Name.Contains(_searchQuery, StringComparison.OrdinalIgnoreCase)); + } + else + { + _organizations = followedOrganizations; + } + + _isLoading = false; + StateHasChanged(); + } + catch (Exception ex) + { + Console.WriteLine($"Error fetching followed organizations: {ex.Message}"); + _isLoading = false; + } + } + private void NavigateToOrganization(Guid organizationId) { NavigationManager.NavigateTo($"/organizations/details/{organizationId}"); } + + private string GetOrganizationImage(string imageUrl) + { + return string.IsNullOrEmpty(imageUrl) + ? "/images/default_organization_profile_image.png" + : imageUrl; + } + + private string GetOrganizationBanner(string bannerUrl) + { + return string.IsNullOrEmpty(bannerUrl) + ? "/images/default_banner_image.png" + : bannerUrl; + } } diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Posts/PostDetails.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Posts/PostDetails.razor index e4ba25683..5521a365f 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Pages/Posts/PostDetails.razor +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Posts/PostDetails.razor @@ -6,6 +6,7 @@ @inject ICommentsService CommentsService @inject NavigationManager NavigationManager @inject ISnackbar Snackbar +@using Microsoft.JSInterop @using MiniSpace.Web.DTO @using MiniSpace.Web.DTO.Wrappers @using MudBlazor @@ -15,6 +16,7 @@ @using System.Threading.Tasks @using System.Collections.Generic + @if (isLoading) { @@ -28,18 +30,21 @@ { - - + + + - - @GetUserName(post.UserId) + + @GetUserName(post.UserId) @post.CreatedAt.ToString("g") - + + + @if (post.MediaFiles != null && post.MediaFiles.Any()) { @foreach (var mediaFile in post.MediaFiles) @@ -48,133 +53,167 @@ } } - @if (reactionsSummary != null) - { - - @foreach (var reaction in reactionsSummary.ReactionsWithCounts.OrderByDescending(r => r.Value)) - { - - - @reaction.Value + + + + + + + @foreach (var reaction in reactionsSummary.ReactionsWithCounts.OrderByDescending(r => r.Value)) + { + + + @reaction.Value + + } + + + + + + + + + + + + + Facebook + + + Twitter + + + LinkedIn + + + WhatsApp + + - } - - Total: @reactionsSummary.NumberOfReactions - - - } + + + + @foreach (var reactionType in Enum.GetValues(typeof(ReactionType)).Cast()) + { + + @reactionType.GetReactionText() + + } + + + Apply Reaction + + + + + + + + - - - Comment - - - - - - React - - - - @foreach (var reactionType in Enum.GetValues(typeof(ReactionType)).Cast()) - { - - @reactionType.GetReactionText() - - } - - - + + + + Submit - @if (isCommentSectionVisible) - { - - - Submit + - @if (comments.Any()) - { - - @foreach (var comment in comments.Where(c => c.ParentId == Guid.Empty)) + Total Comments: @totalComments + + +
+ @if (comments.Any()) + { + + @foreach (var comment in comments) + { + + + + + + + @GetUserName(comment.UserId) + @comment.TextContent + @comment.CreatedAt.ToString("g") + + + @($"{comment.Likes?.Count() ?? 0} people liked this comment") + + + + + + + + + + + + + + @if (comment.Id == activeReplyCommentId) + { + + Submit Reply + } + + @if (comments.Any(c => c.ParentId == comment.Id)) + { + + @foreach (var reply in comments.Where(c => c.ParentId == comment.Id)) + { + + + + + + + @GetUserName(reply.UserId) + @reply.TextContent + @reply.CreatedAt.ToString("g") + + + @($"{reply.Likes?.Count() ?? 0} people liked this reply") + + + + + + + + + } + + } + + + + } + + + @if (!isLastPage) { - - - - - - - @GetUserName(comment.UserId) - @comment.TextContent - @comment.CreatedAt.ToString("g") - - - - @($"{comment.Likes?.Count() ?? 0} people liked this comment") - - - - - Like - - - - Reply - - - @if (comment.Id == activeReplyCommentId) - { - - Submit Reply - } - - @if (comments.Any(c => c.ParentId == comment.Id)) - { - - @foreach (var reply in comments.Where(c => c.ParentId == comment.Id)) - { - - - - - - - @GetUserName(reply.UserId) - @reply.TextContent - @reply.CreatedAt.ToString("g") - - - - @($"{reply.Likes?.Count() ?? 0} people liked this reply") - - - - - Like - - - - - } - - } - - - + + Load More Comments + } - - } - else - { - No comments available. - } - - } + } + else + { + No comments available. + } +
+
+
}
+
@code { [Parameter] public Guid postId { get; set; } @@ -184,10 +223,15 @@ private List comments = new(); private Dictionary studentsCache = new(); private bool isLoading = true; - private bool isCommentSectionVisible = false; private Guid? activeReplyCommentId; private string newCommentText = string.Empty; private string newReplyText = string.Empty; + private int currentPage = 1; + private int pageSize = 10; + private int totalComments = 0; + private bool isLastPage = false; + private ElementReference commentSection; + private ReactionType selectedReaction = ReactionType.LikeIt; protected override async Task OnInitializedAsync() { @@ -219,10 +263,16 @@ private async Task LoadPostDetailsAsync() { - post = await PostsService.GetPostAsync(postId); - - if (post != null) + try { + post = await PostsService.GetPostAsync(postId); + + if (post == null) + { + Snackbar.Add("Post not found.", Severity.Warning); + return; + } + if (post.UserId.HasValue) { var student = await StudentsService.GetStudentAsync(post.UserId.Value); @@ -233,11 +283,85 @@ } reactionsSummary = await ReactionsService.GetReactionsSummaryAsync(postId, ReactionContentType.Post); - comments = await LoadCommentsForPostAsync(postId); + await LoadCommentsAsync(); + } + catch (Exception ex) + { + Snackbar.Add($"Failed to load post details: {ex.Message}", Severity.Error); + } + } + + private void CacheStudentInfo(IEnumerable commentDtos) + { + foreach (var comment in commentDtos) + { + if (!studentsCache.ContainsKey(comment.UserId)) + { + var student = StudentsService.GetStudentAsync(comment.UserId).Result; + if (student != null) + { + studentsCache[comment.UserId] = student; + } + } + + if (comment.Replies != null) + { + foreach (var reply in comment.Replies) + { + if (!studentsCache.ContainsKey(reply.UserId)) + { + var replyAuthor = StudentsService.GetStudentAsync(reply.UserId).Result; + if (replyAuthor != null) + { + studentsCache[reply.UserId] = replyAuthor; + } + } + } + } + } + } + + private async Task LoadMoreCommentsAsync() + { + if (!isLastPage) + { + currentPage++; + await LoadCommentsAsync(); + } + } + + private async Task LoadCommentsAsync() + { + var command = new SearchRootCommentsCommand( + contextId: postId, + commentContext: DetermineCommentContext().ToString(), + pageable: new PageableDto + { + Page = currentPage, + Size = pageSize, + Sort = new SortDto + { + SortBy = new[] { "CreatedAt" }, + Direction = "asc" + } + } + ); + + var response = await CommentsService.SearchRootCommentsAsync(command); + + if (response != null) + { + // Update total comments and check if it is the last page + totalComments = response.TotalItems; + isLastPage = currentPage * pageSize >= totalComments; + + // Append new comments to the existing list + comments.AddRange(response.Items); + CacheStudentInfo(response.Items); } else { - Snackbar.Add("Post not found.", Severity.Warning); + Snackbar.Add("Failed to load comments.", Severity.Error); } } @@ -281,6 +405,7 @@ if (updateResult.IsSuccessStatusCode) { Snackbar.Add("Reaction updated successfully!", Severity.Success); + reactionsSummary = await ReactionsService.GetReactionsSummaryAsync(postId, ReactionContentType.Post); } else { @@ -303,14 +428,13 @@ if (createResult.IsSuccessStatusCode) { Snackbar.Add("Reaction added successfully!", Severity.Success); + reactionsSummary = await ReactionsService.GetReactionsSummaryAsync(postId, ReactionContentType.Post); } else { Snackbar.Add($"Failed to add reaction: {createResult.ErrorMessage?.Reason}", Severity.Error); } } - - reactionsSummary = await ReactionsService.GetReactionsSummaryAsync(postId, ReactionContentType.Post); } private async Task GetExistingReactionAsync(Guid postId) @@ -319,12 +443,6 @@ return reactions.FirstOrDefault(r => r.UserId == IdentityService.UserDto.Id); } - private void ToggleCommentSection() - { - isCommentSectionVisible = !isCommentSectionVisible; - newCommentText = string.Empty; - } - private void ToggleReplySection(Guid commentId) { if (activeReplyCommentId == commentId) @@ -339,56 +457,6 @@ } } - private async Task> LoadCommentsForPostAsync(Guid postId) - { - var command = new SearchRootCommentsCommand( - contextId: postId, - commentContext: DetermineCommentContext().ToString(), - pageable: new PageableDto - { - Page = 1, - Size = 10, - Sort = new SortDto - { - SortBy = new[] { "CreatedAt" }, - Direction = "asc" - } - } - ); - - var response = await CommentsService.SearchRootCommentsAsync(command); - var comments = response.Items?.ToList() ?? new List(); - - foreach (var comment in comments) - { - if (!studentsCache.ContainsKey(comment.UserId)) - { - var student = await StudentsService.GetStudentAsync(comment.UserId); - if (student != null) - { - studentsCache[comment.UserId] = student; - } - } - - if (comment.Replies != null) - { - foreach (var reply in comment.Replies) - { - if (!studentsCache.ContainsKey(reply.UserId)) - { - var replyAuthor = await StudentsService.GetStudentAsync(reply.UserId); - if (replyAuthor != null) - { - studentsCache[reply.UserId] = replyAuthor; - } - } - } - } - } - - return comments; - } - private CommentContext DetermineCommentContext() { if (post.OrganizationId.HasValue) @@ -424,7 +492,9 @@ { Snackbar.Add("Comment added successfully!", Severity.Success); newCommentText = string.Empty; - comments = await LoadCommentsForPostAsync(postId); + currentPage = 1; // Reset to the first page + comments.Clear(); // Clear the comments list + await LoadCommentsAsync(); } else { @@ -455,7 +525,9 @@ { Snackbar.Add("Reply added successfully!", Severity.Success); newReplyText = string.Empty; - comments = await LoadCommentsForPostAsync(postId); + currentPage = 1; // Reset to the first page + comments.Clear(); // Clear the comments list + await LoadCommentsAsync(); } else { @@ -472,7 +544,6 @@ if (response.IsSuccessStatusCode) { Snackbar.Add("Comment liked successfully!", Severity.Success); - comments = await LoadCommentsForPostAsync(postId); // Refresh comments } else { @@ -489,11 +560,33 @@ if (response.IsSuccessStatusCode) { Snackbar.Add("Reply liked successfully!", Severity.Success); - comments = await LoadCommentsForPostAsync(postId); // Refresh comments } else { Snackbar.Add($"Failed to like reply: {response.ErrorMessage?.Reason}", Severity.Error); } } + + private void SharePost(string platform) + { + var postUrl = NavigationManager.Uri; + var encodedUrl = Uri.EscapeDataString(postUrl); + var shareUrl = platform switch + { + "facebook" => $"https://www.facebook.com/sharer/sharer.php?u={encodedUrl}", + "twitter" => $"https://twitter.com/intent/tweet?url={encodedUrl}", + "linkedin" => $"https://www.linkedin.com/shareArticle?mini=true&url={encodedUrl}", + "whatsapp" => $"https://api.whatsapp.com/send?text={encodedUrl}", + _ => "" + }; + + if (!string.IsNullOrEmpty(shareUrl)) + { + NavigationManager.NavigateTo(shareUrl, true); + } + else + { + Snackbar.Add("Failed to share the post.", Severity.Error); + } + } } diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Reports/Dialogs/CreateReportDialog.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Reports/Dialogs/CreateReportDialog.razor deleted file mode 100644 index 0c4f97bd6..000000000 --- a/MiniSpace.Web/src/MiniSpace.Web/Pages/Reports/Dialogs/CreateReportDialog.razor +++ /dev/null @@ -1,82 +0,0 @@ -@page "/reports/create/dialog" -@using MiniSpace.Web.Areas.Reports -@using MiniSpace.Web.Models.Reports -@using Radzen -@inject DialogService DialogService -@inject IReportsService ReportsService - - - @errorMessage - - - - - - - - - - - - - - - - - - - - - - - - - - - -@code { - [Parameter] - public CreateReportModel CreateReportModel { get; set; } - - private bool showError = false; - private string errorMessage = string.Empty; - - private readonly List> categories = - [ - new KeyValuePair("Spam", "Spam"), - new KeyValuePair("Harassment and bullying", "HarassmentAndBullying"), - new KeyValuePair("Violence", "Violence"), - new KeyValuePair("Sexual content", "SexualContent"), - new KeyValuePair("Misinformation", "Misinformation"), - new KeyValuePair("Privacy violations", "PrivacyViolations"), - new KeyValuePair("Intellectual property violations", "IntellectualPropertyViolations"), - new KeyValuePair("Other violations", "OtherViolations") - ]; - - protected override async Task OnInitializedAsync() - { - await base.OnInitializedAsync(); - } - - private async Task CreateReport(CreateReportModel createReportModel) - { - var response = await ReportsService.CreateReportAsync(Guid.Empty, createReportModel.IssuerId, - createReportModel.TargetId, createReportModel.TargetOwnerId, createReportModel.ContextType, - createReportModel.Category, CreateReportModel.Reason); - - if (response.ErrorMessage != null) - { - showError = true; - errorMessage = $"Error during reporting: {response.ErrorMessage.Reason}"; - } - else - { - DialogService.Close(true); - } - } -} \ No newline at end of file diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/_Host.cshtml b/MiniSpace.Web/src/MiniSpace.Web/Pages/_Host.cshtml index 68b313dff..4f1f53da1 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Pages/_Host.cshtml +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/_Host.cshtml @@ -219,10 +219,96 @@ }); } + + function initializeCommunicationSignalR() { + if (!userId) return; + + const communicationConnection = new signalR.HubConnectionBuilder() + .withUrl(`http://localhost:5016/chatHub?userId=${userId}`) + .withAutomaticReconnect([0, 2000, 10000, 30000]) // Retry delays + .build(); + + communicationConnection.on("ReceiveMessage", function (jsonMessage) { + console.log("Message received: ", jsonMessage); + DotNet.invokeMethodAsync('MiniSpace.Web', 'ReceiveMessage', jsonMessage); + }); + + communicationConnection.start().then(function () { + console.log("Communication SignalR connection established."); + }).catch(function (err) { + console.error("Error establishing SignalR connection: ", err.toString()); + }); + + communicationConnection.onreconnecting((error) => { + console.warn(`Connection lost due to error "${error}". Reconnecting.`); + }); + + communicationConnection.onreconnected((connectionId) => { + console.log(`Connection reestablished. Connected with connectionId "${connectionId}".`); + }); + + communicationConnection.onclose((error) => { + console.error(`Connection closed due to error "${error}". Attempting to reconnect...`); + setTimeout(() => initializeCommunicationSignalR(), 5000); + }); + } + + window.getSystemTheme = () => { return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; }; + window.scrollToBottom = function (elementId) { + var element = document.getElementById(elementId); + if (element) { + element.scrollTop = element.scrollHeight; + } + }; + + window.initializeInfiniteScroll = (element) => { + if (!element || !(element instanceof Element)) { + console.warn('Element for infinite scroll is not defined or is not a valid DOM element.'); + return; + } + + const observer = new IntersectionObserver(entries => { + entries.forEach(entry => { + if (entry.isIntersecting) { + // Call Blazor method to load more comments + DotNet.invokeMethodAsync('MiniSpace.Web', 'LoadMoreCommentsAsync') + .catch(err => console.error('Error invoking Blazor method:', err)); + } + }); + }, { + threshold: 1.0 + }); + + // Disconnect any existing observer to avoid multiple triggers + const lastChild = element.lastElementChild; + if (lastChild) { + observer.observe(lastChild); + } +}; + +window.getDeviceType = () => { + const ua = navigator.userAgent; + if (/android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini/i.test(ua)) { + return "Mobile"; + } + if (/tablet|ipad/i.test(ua)) { + return "Tablet"; + } + return "Desktop"; +}; + + + + + + + + + @@ -230,7 +316,6 @@ - diff --git a/MiniSpace.Web/src/MiniSpace.Web/Shared/AuthenticatedLayout.razor b/MiniSpace.Web/src/MiniSpace.Web/Shared/AuthenticatedLayout.razor deleted file mode 100644 index 09940b9df..000000000 --- a/MiniSpace.Web/src/MiniSpace.Web/Shared/AuthenticatedLayout.razor +++ /dev/null @@ -1,79 +0,0 @@ -@using Microsoft.AspNetCore.Components.Authorization -@using Radzen -@using MiniSpace.Web.Areas.Students -@inherits LayoutComponentBase -@inject IIdentityService IdentityService -@inject IStudentsService StudentsService -@inject NavigationManager NavigationManager -@inject Blazored.LocalStorage.ILocalStorageService localStorage - - - - - - - - -

Sorry, there's nothing at this address.

-
-
-
-
- - -
- - - @if (IsUserAuthenticated() && StudentsService.StudentDto.State == "valid") - { - - } -
- -
-
- - -
- -
-
-
-
-
- @if (IsUserAuthenticated() && StudentsService.StudentDto.State == "valid") - { - - - - - - - - - } - -
- @Body -
-
-
- -
- - -@code { - bool _sidebarExpanded = true; - - public bool IsUserAuthenticated() => IdentityService.IsAuthenticated; - - void NavigateToHome() { - NavigationManager.NavigateTo("/home"); - } - - async Task SignOut() { - await localStorage.RemoveItemAsync("accessToken"); - await localStorage.RemoveItemAsync("jwtDto"); - NavigationManager.NavigateTo("signin", forceLoad: true); - } -} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Shared/FooterComponent.razor b/MiniSpace.Web/src/MiniSpace.Web/Shared/FooterComponent.razor index 3441d353f..07136f38d 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Shared/FooterComponent.razor +++ b/MiniSpace.Web/src/MiniSpace.Web/Shared/FooterComponent.razor @@ -3,6 +3,7 @@ @inherits LayoutComponentBase + + diff --git a/MiniSpace.Web/src/MiniSpace.Web/Shared/MainLayout.razor b/MiniSpace.Web/src/MiniSpace.Web/Shared/MainLayout.razor index 3c4385695..6fe90ad3c 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Shared/MainLayout.razor +++ b/MiniSpace.Web/src/MiniSpace.Web/Shared/MainLayout.razor @@ -47,8 +47,8 @@
- Profile - Settings + Profile + Settings Account settings Notifications Sign Out @@ -118,11 +118,18 @@ Organizations I Follow + + All Chats + New Chat + Chat History + + All New History + Reports @if (IdentityService.GetCurrentUserRole() == "admin") { @@ -138,20 +145,20 @@
- - - @Body + + @Body - @if (_isUserAuthenticated) - { -
- -
- } + @if (_isUserAuthenticated) + { +
+ +
+ } -
+
+ @@ -259,7 +266,7 @@ { if (NavigationManager.Uri != NavigationManager.BaseUri) { - NavigationManager.NavigateTo("/", true); + NavigationManager.NavigateTo("/home", true); } while (NavigationManager.Uri != NavigationManager.BaseUri) { @@ -270,7 +277,7 @@ void NavigateToHome() { - NavigationManager.NavigateTo("/"); + NavigationManager.NavigateTo("/home"); } private string _themeName = "light"; diff --git a/MiniSpace.Web/src/MiniSpace.Web/Shared/NotAuthenticatedLayout.razor b/MiniSpace.Web/src/MiniSpace.Web/Shared/NotAuthenticatedLayout.razor deleted file mode 100644 index 34f8a1255..000000000 --- a/MiniSpace.Web/src/MiniSpace.Web/Shared/NotAuthenticatedLayout.razor +++ /dev/null @@ -1,50 +0,0 @@ -@using Radzen -@using MiniSpace.Web.Areas.Students -@inherits LayoutComponentBase -@inject IIdentityService IdentityService -@inject NavigationManager NavigationManager -@inject Microsoft.JSInterop.IJSRuntime JSRuntime - - -
- - -
- -
-
- -
- - - - - -
-
-
-
- -
- @Body -
-
- -
- - -@code { - void NavigateToHome() { - NavigationManager.NavigateTo("/"); - } - - async Task ScrollToSection(string sectionId) { - if (NavigationManager.Uri != NavigationManager.BaseUri) { - NavigationManager.NavigateTo("/", true); - } - while (NavigationManager.Uri != NavigationManager.BaseUri) { - await Task.Delay(100); - } - await JSRuntime.InvokeVoidAsync("scrollToSection", sectionId); - } -} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Startup.cs b/MiniSpace.Web/src/MiniSpace.Web/Startup.cs index da3ef5f0e..a96fd4261 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Startup.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/Startup.cs @@ -29,6 +29,7 @@ using MiniSpace.Web.Areas.Reactions; using MiniSpace.Web.Areas.Reports; using Microsoft.AspNetCore.Server.Kestrel.Core; +using MiniSpace.Web.Areas.Communication; namespace MiniSpace.Web { @@ -105,9 +106,11 @@ public void ConfigureServices(IServiceCollection services) services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); } diff --git a/MiniSpace.Web/src/MiniSpace.Web/_Imports.razor b/MiniSpace.Web/src/MiniSpace.Web/_Imports.razor index b8bed2042..dc6414380 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/_Imports.razor +++ b/MiniSpace.Web/src/MiniSpace.Web/_Imports.razor @@ -16,7 +16,6 @@ @using MiniSpace.Web.Areas.MediaFiles @using MiniSpace.Web.Areas.Organizations -@using Radzen.Blazor @using Cropper.Blazor.Components @using MiniSpace.Web.DTO @@ -45,4 +44,11 @@ @using MiniSpace.Web.Areas.Friends.CommandsDto @using MiniSpace.Web.Areas.Friends -@using MiniSpace.Web.DTO.Friends \ No newline at end of file +@using MiniSpace.Web.DTO.Friends +@using MiniSpace.Web.DTO.States; + +@using MiniSpace.Web.Areas.Communication +@using MiniSpace.Web.DTO.Communication +@using MiniSpace.Web.Areas.Communication.CommandsDto + +@using MiniSpace.Web.DTO.Users \ No newline at end of file diff --git a/MiniSpace.Web/src/MiniSpace.Web/wwwroot/css/site.css b/MiniSpace.Web/src/MiniSpace.Web/wwwroot/css/site.css index 61b2b7355..6c0eaf2f6 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/wwwroot/css/site.css +++ b/MiniSpace.Web/src/MiniSpace.Web/wwwroot/css/site.css @@ -131,7 +131,7 @@ app { .main-container { display: flex; flex-direction: column; - height: 100vh; + min-height: 100vh; } /* header { @@ -914,4 +914,41 @@ div.connectionRejected { left: 16px; border: 3px solid white; box-shadow: 0px 2px 10px rgba(0, 0, 0, 0.2); -} \ No newline at end of file +} + + /* Mobile-specific styles */ + @media (max-width: 800px) { + .chat-name { + display: none !important; + } + } + @media (max-width: 600px) { + /* .chat-name { + display: none !important; + } */ + + .conversation-list-container { + width: 60px; + padding: 0; + } + + .conversation-list .MudListItemIcon { + margin-right: 0; + } + + .chat-container { + padding: 0 8px; + } +} + +.typing-indicator { + font-style: italic; + color: #555; + margin-bottom: 10px; +} + + +.mud-dialog-blur-backdrop { + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); +}