Social: Add enhancements in user invitations and relationships management - refs BT#21101

pull/5171/head
christianbeeznst 2 years ago
parent 8e2905d6cd
commit 127fa42bfa
  1. 269
      assets/css/scss/_social.scss
  2. 1
      assets/vue/components/basecomponents/ChamiloIcons.js
  3. 1
      assets/vue/components/social/GroupInfoCard.vue
  4. 40
      assets/vue/components/social/SocialGroupMenu.vue
  5. 36
      assets/vue/components/social/SocialSideMenu.vue
  6. 16
      assets/vue/components/usergroup/GroupDiscussions.vue
  7. 7
      assets/vue/components/usergroup/GroupMembers.vue
  8. 61
      assets/vue/components/userreluser/InvitationList.vue
  9. 77
      assets/vue/composables/useSocialInfo.js
  10. 5
      assets/vue/router/social.js
  11. 5
      assets/vue/router/userreluser.js
  12. 17
      assets/vue/views/social/SocialLayout.vue
  13. 292
      assets/vue/views/social/SocialSearch.vue
  14. 70
      assets/vue/views/usergroup/Show.vue
  15. 114
      assets/vue/views/userreluser/Invitations.vue
  16. 268
      src/CoreBundle/Controller/SocialController.php
  17. 3
      src/CoreBundle/Entity/Usergroup.php
  18. 144
      src/CoreBundle/Repository/MessageRepository.php
  19. 174
      src/CoreBundle/Repository/Node/UserRepository.php
  20. 177
      src/CoreBundle/Repository/Node/UsergroupRepository.php

@ -431,7 +431,7 @@
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 0;
padding: 20px 0;
border-bottom: 1px solid #ccc;
}
@ -460,13 +460,10 @@
align-items: center;
}
.author-avatar-icon {
font-size: 50px;
margin-right: 10px;
}
.author-name {
font-size: 0.9rem;
.author-avatar img, .author-avatar .mdi {
border-radius: 50%;
width: 50px;
height: 50px;
}
.author-avatar {
@ -474,41 +471,26 @@
height: 30px;
border-radius: 50%;
margin-right: 10px;
}
.author-avatar-icon {
font-size: 30px;
margin-right: 10px;
display: flex;
justify-content: center;
}
.author-name {
font-size: 0.9em;
}
.discussion-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 0;
text-align: center;
margin-top: 10px;
font-size: 0.9rem;
margin-right: 10px;
}
.discussion-author {
display: flex;
flex-direction: column;
align-items: center;
}
.author-name {
margin-right: 10px;
}
.author-avatar {
width: 30px;
height: 30px;
border-radius: 50%;
}
.author-avatar-icon {
font-size: 30px;
font-size: 50px;
margin-right: 10px;
}
.discussions-container {
@ -557,13 +539,15 @@
}
.member-avatar {
margin-bottom: 10px;
border-radius: 50%;
margin:auto;
}
.member-avatar img {
width: 100px;
height: 100px;
border-radius: 50%;
margin: auto;
}
.member-avatar i {
@ -585,13 +569,6 @@
align-items: center;
}
.member-avatar {
width: 50px;
height: 50px;
border-radius: 50%;
margin-right: 16px;
}
.member-name {
font-size: 1.2em;
color: #333;
@ -777,3 +754,215 @@
padding: 1px;
}
}
.social-search {
.invitation-modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
background-color: rgba(0, 0, 0, 0.6);
}
.invitation-modal {
background: white;
width: 90%;
max-width: 500px;
border-radius: 8px;
padding: 20px;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
}
.invitation-modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.close-button {
border: none;
background: none;
font-size: 24px;
cursor: pointer;
}
.invitation-modal-textarea {
width: 100%;
height: 100px;
padding: 10px;
margin-bottom: 20px;
border: 1px solid #ccc;
border-radius: 4px;
resize: none;
}
.invitation-modal-send {
width: 100%;
padding: 10px 20px;
border: none;
background-color: #007bff;
color: white;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
}
.invitation-modal-send:hover {
background-color: #0056b3;
}
.group-card {
background: #fff;
border-radius: 0.5rem;
overflow: hidden;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.group-image img {
width: 100%;
height: auto;
display: block;
}
.group-info {
padding: 1rem;
text-align: center;
}
.group-info h3 {
margin-top: 1rem;
font-size: 1.25rem;
}
.group-info p {
font-size: 0.875rem;
color: #666;
}
.message-modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.message-modal {
background: #FFFFFF;
border-radius: 8px;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
padding: 20px;
width: 400px;
position: relative;
}
.message-modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.message-modal-close {
background: none;
border: none;
font-size: 20px;
cursor: pointer;
}
.message-user-info {
display: flex;
align-items: center;
margin-bottom: 15px;
}
.message-user-avatar {
width: 50px;
height: 50px;
border-radius: 50%;
margin-right: 10px;
}
.message-user-name {
font-weight: bold;
}
.message-modal-input,
.message-modal-textarea {
width: 100%;
padding: 10px;
margin-bottom: 15px;
border: 1px solid #CCC;
border-radius: 4px;
}
.message-modal-textarea {
height: 100px;
resize: vertical;
}
.message-modal-send {
width: 100%;
padding: 10px;
border: none;
background-color: #007bff;
color: white;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
}
.message-modal-send:hover {
background-color: #0056b3;
}
}
.friends-invitations {
.invitation-list {
width: 100%;
}
.invitation-item {
border-bottom: 1px solid #eee;
padding-bottom: 1rem;
margin-bottom: 1rem;
}
.invitation-content {
display: flex;
align-items: center;
justify-content: space-between;
}
.item-picture {
width: 50px;
height: 50px;
border-radius: 50%;
margin-right: 15px;
}
.invitation-info {
flex-grow: 1;
}
.invitation-actions {
display: flex;
align-items: center;
gap: 0.5rem;
}
@media (max-width: 600px) {
.invitation-actions {
flex-direction: column;
}
}
}

@ -114,4 +114,5 @@ export const chamiloIconToClass = {
"template-selected": "mdi mdi-file-check",
"template-not-selected": "mdi mdi-file-outline",
"map-search": "mdi mdi-map-search-outline",
"join-group": "mdi mdi-account-multiple-plus",
};

@ -8,6 +8,7 @@
/>
<hr />
<BaseButton
v-if="groupInfo.isModerator"
:label="t('Edit this group')"
type="primary"
class="mt-4"

@ -6,7 +6,7 @@
</div>
</template>
<hr class="-mt-2 mb-4 -mx-4">
<ul class="menu-list">
<ul v-if="groupInfo.isMember" class="menu-list">
<li class="menu-item">
<router-link to="/social">
<i class="mdi mdi-home" aria-hidden="true"></i>
@ -26,9 +26,18 @@
</router-link>
</li>
<li class="menu-item">
<router-link :to="{ name: '', params: { group_id: groupInfo.id } }">
<button @click="leaveGroup">
<i class="mdi mdi-exit-to-app" aria-hidden="true"></i>
{{ t("Leave group") }}
</button>
</li>
</ul>
<ul v-else>
<li class="menu-item">
<router-link to="/social">
<i class="mdi mdi-home" aria-hidden="true"></i>
{{ t("Home") }}
</router-link>
</li>
</ul>
@ -37,20 +46,39 @@
<script setup>
import BaseCard from "../basecomponents/BaseCard.vue"
import { useRoute } from 'vue-router'
import { useRoute, useRouter } from "vue-router"
import { useI18n } from "vue-i18n"
import { onMounted, computed, ref, inject, watchEffect } from "vue"
import { useStore } from "vuex"
import { useSecurityStore } from "../../store/securityStore"
import axios from 'axios'
import { useNotification } from "../../composables/notification"
import { useSocialInfo } from "../../composables/useSocialInfo"
const { t } = useI18n()
const route = useRoute()
const router = useRouter()
const store = useStore()
const securityStore = useSecurityStore()
const notification = useNotification()
const groupInfo = inject('group-info')
const isGroup = inject('is-group')
const { user, groupInfo, isGroup, loadGroup, isLoading } = useSocialInfo()
const leaveGroup = async () => {
try {
const response = await axios.post('/social-network/group-action', {
userId: user.value.id,
groupId: groupInfo.value.id,
action: 'leave'
})
if (response.data.success) {
notification.showSuccessNotification(t('You have left the group successfully'))
router.push('/social')
}
} catch (error) {
console.error('Error leaving the group:', error)
}
}
const isActive = (path, filterType = null) => {
const pathMatch = route.path.startsWith(path)
const hasQueryParams = Object.keys(route.query).length > 0

@ -20,11 +20,10 @@
<span class="badge badge-warning">{{ unreadMessagesCount }}</span>
</router-link>
</li>
<li :class="['menu-item', { 'active': isActive('/resources/personal_files') }]">
<router-link :to="{ name: 'PersonalFileList', params: { node: currentNodeId } }">
<i class="mdi mdi-briefcase"></i>
{{ t("My files") }}
<li :class="['menu-item', { 'active': isActive('/resources/friends/invitations') }]">
<router-link :to="{ name: 'Invitations' }">
<i class="mdi mdi-mailbox" aria-hidden="true"></i> <!-- Cambiado a mdi-invitation -->
{{ t("Invitations") }}
</router-link>
</li>
<li :class="['menu-item', { 'active': isActive('/account/home') }]">
@ -49,10 +48,16 @@
{{ t("Social groups") }}
</router-link>
</li>
<li :class="['menu-item', { 'active': isActive('/social', 'promoted') }]">
<router-link :to="{ path: '/social', query: { filterType: 'promoted' } }">
<i class="mdi mdi-star" aria-hidden="true"></i>
{{ t("Promoted messages") }}
<li :class="['menu-item', { 'active': isActive('/social/search') }]">
<router-link to="/social/search">
<i class="mdi mdi-magnify" aria-hidden="true"></i>
{{ t("Search") }}
</router-link>
</li>
<li :class="['menu-item', { 'active': isActive('/resources/personal_files') }]">
<router-link :to="{ name: 'PersonalFileList', params: { node: currentNodeId } }">
<i class="mdi mdi-briefcase"></i>
{{ t("My files") }}
</router-link>
</li>
<li :class="['menu-item', { 'active': isActive('/resources/users/personal_data') }]">
@ -61,6 +66,12 @@
{{ t("Personal data") }}
</router-link>
</li>
<li :class="['menu-item', { 'active': isActive('/social', 'promoted') }]">
<router-link :to="{ path: '/social', query: { filterType: 'promoted' } }">
<i class="mdi mdi-star" aria-hidden="true"></i>
{{ t("Promoted messages") }}
</router-link>
</li>
</ul>
<ul v-else class="menu-list">
<li class="menu-item">
@ -122,12 +133,15 @@ const getGroupLink = async () => {
}
const isActive = (path, filterType = null) => {
if (path === '/resources/friends/invitations') {
return route.path === path
}
const pathMatch = route.path.startsWith(path)
const hasQueryParams = Object.keys(route.query).length > 0
const filterMatch = filterType ? (route.query.filterType === filterType && hasQueryParams) : !hasQueryParams
return pathMatch && filterMatch
return pathMatch && filterMatch && !route.path.startsWith('/resources/friends/invitations')
}
watchEffect(() => {
try {
if (user.value && user.value.resourceNode) {

@ -13,13 +13,15 @@
<i class="mdi mdi-message-reply-text icon"></i>
<span>{{ discussion.repliesCount }} {{ t("Replies") }}</span>
<i class="mdi mdi-clock-outline icon"></i>
<span>Created {{ new Date(discussion.sendDate).toLocaleDateString() }}</span>
<span>{{ t("Created") }} {{ relativeDatetime(discussion.sendDate) }}</span>
</div>
</div>
<div class="discussion-author">
<img v-if="discussion.sender.illustrationUrl" :src="discussion.sender.illustrationUrl" class="author-avatar-icon">
<i v-else class="mdi mdi-account-circle-outline author-avatar-icon"></i>
<span class="author-name">{{ discussion.sender.name }}</span>
<div class="author-avatar">
<img v-if="discussion.sender.illustrationUrl" :src="discussion.sender.illustrationUrl" alt="Author avatar">
<i v-else class="mdi mdi-account-circle-outline"></i>
</div>
<div class="author-name mt-4">{{ discussion.sender.username }}</div>
</div>
</div>
</div>
@ -30,11 +32,15 @@ import { ref, onMounted, computed } from 'vue'
import { useRoute } from 'vue-router'
import axios from 'axios'
import { useI18n } from "vue-i18n"
import { useFormatDate } from "../../composables/formatDate"
import { useSocialInfo } from "../../composables/useSocialInfo"
const route = useRoute()
const discussions = ref([])
const groupId = ref(route.params.group_id)
const { t } = useI18n()
const { relativeDatetime } = useFormatDate()
const { user, groupInfo, isGroup, loadGroup, isLoading } = useSocialInfo()
onMounted(async () => {
if (groupId.value) {
try {

@ -1,6 +1,6 @@
<template>
<div class="group-members">
<div class="edit-members">
<div v-if="groupInfo.isModerator" class="edit-members">
<BaseButton
label="Edit members list"
type="primary"
@ -30,11 +30,12 @@ import { ref, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import BaseButton from "../basecomponents/BaseButton.vue"
import axios from "axios"
import { useSocialInfo } from "../../composables/useSocialInfo"
const route = useRoute()
const members = ref([])
const groupId = ref(route.params.group_id)
const { user, groupInfo, isGroup, loadGroup, isLoading } = useSocialInfo()
const fetchMembers = async (groupId) => {
if (groupId.value) {
try {
@ -43,7 +44,7 @@ const fetchMembers = async (groupId) => {
id: member.id,
name: member.username,
role: member.relationType === 1 ? 'Admin' : 'Member',
avatar: null,
avatar: member.pictureUri,
isAdmin: member.relationType === 1
}))
} catch (error) {

@ -0,0 +1,61 @@
<template>
<div class="friends-invitations">
<BaseCard plain class="bg-white mt-4">
<template #header>
<div class="px-4 py-2 bg-gray-15">
<h2 class="text-h5">{{ title }}</h2>
</div>
</template>
<hr class="my-4">
<div v-if="invitations && invitations.length > 0" class="invitation-list">
<div v-for="invitation in invitations" :key="invitation.id" class="invitation-item">
<div class="invitation-content">
<img :src="invitation.itemPicture" class="item-picture" alt="Item picture">
<div class="invitation-info">
<h4><a :href="'profile.php?u=' + invitation.itemId">{{ invitation.itemName }}</a></h4>
<p>{{ invitation.content }}</p>
<span>{{ invitation.date }}</span>
</div>
<div class="invitation-actions">
<BaseButton
v-if="invitation.canAccept"
label="Accept"
icon="check"
type="success"
@click="emitEvent('accept', invitation.id)"
/>
<BaseButton
v-if="invitation.canDeny"
label="Deny"
icon="times"
type="danger"
@click="emitEvent('deny', invitation.id)"
/>
</div>
</div>
</div>
</div>
<div v-else class="no-invitations-message">
<p>{{ t("No invitations or records found") }}</p>
</div>
</BaseCard>
</div>
</template>
<script setup>
import BaseCard from "../basecomponents/BaseCard.vue"
import BaseButton from "../basecomponents/BaseButton.vue"
import { useI18n } from "vue-i18n"
import { useFormatDate } from "../../composables/formatDate"
const { t } = useI18n()
const { relativeDatetime } = useFormatDate()
const props = defineProps({
invitations: Array,
title: String
})
const emit = defineEmits(['accept', 'deny'])
function emitEvent(event, id) {
emit(event, id)
}
</script>

@ -1,45 +1,42 @@
import { ref, readonly, onMounted } from "vue";
import { useStore } from "vuex";
import { useRoute } from "vue-router";
import axios from "axios";
import { ref, readonly, onMounted } from "vue"
import { useStore } from "vuex"
import { useRoute } from "vue-router"
import axios from "axios"
export function useSocialInfo() {
const store = useStore();
const route = useRoute();
const user = ref({});
const isCurrentUser = ref(true);
const groupInfo = ref({});
const isGroup = ref(false);
const isLoading = ref(true);
const store = useStore()
const route = useRoute()
const user = ref({})
const isCurrentUser = ref(true)
const groupInfo = ref({
isMember: false,
title: '',
description: '',
role: ''
})
const isGroup = ref(false)
const isLoading = ref(true)
const loadGroup = async (groupId) => {
isLoading.value = true;
isLoading.value = true
if (groupId) {
try {
const response = await axios.get(`/api/usergroup/${groupId}`);
const groupData = response.data;
const extractedId = groupData['@id'].split('/').pop();
const response = await axios.get(`/social-network/group-details/${groupId}`)
groupInfo.value = {
...groupData,
id: extractedId
};
isGroup.value = true;
...response.data,
isMember: response.data.isMember,
role: response.data.role,
}
isGroup.value = true
} catch (error) {
console.error("Error loading group:", error);
groupInfo.value = {};
isGroup.value = false;
console.error("Error loading group:", error)
groupInfo.value = {}
isGroup.value = false
}
isLoading.value = false;
isLoading.value = false
} else {
isGroup.value = false;
groupInfo.value = {};
isGroup.value = false
groupInfo.value = {}
}
};
}
const loadUser = async () => {
try {
if (route.query.id) {
@ -53,21 +50,19 @@ export function useSocialInfo() {
user.value = {}
isCurrentUser.value = true
}
};
}
onMounted(async () => {
try {
//if (!route.params.group_id) {
await loadUser();
await loadUser()
//}
if (route.params.group_id) {
await loadGroup(route.params.group_id);
await loadGroup(route.params.group_id)
}
} finally {
isLoading.value = false;
isLoading.value = false
}
});
})
return {
user: readonly(user),
isCurrentUser: readonly(isCurrentUser),
@ -76,5 +71,5 @@ export function useSocialInfo() {
loadGroup,
loadUser,
isLoading,
};
}
}

@ -9,5 +9,10 @@ export default {
path: ':filterType?',
component: () => import('../views/social/SocialWall.vue')
},
{
name: 'SocialSearch',
path: 'search',
component: () => import('../views/social/SocialSearch.vue')
}
]
}

@ -20,6 +20,11 @@ export default {
name: 'UserRelUserSearch',
path: 'search',
component: () => import('../views/userreluser/UserRelUserSearch.vue')
},
{
name: 'Invitations',
path: 'invitations',
component: () => import('../views/userreluser/Invitations.vue')
}
]
};

@ -6,10 +6,10 @@
</div>
<div class="flex-grow w-full md:basis-1/2 lg:basis-2/3">
<SocialNetworkWall />
<component :is="currentComponent" />
</div>
<div class="flex flex-col w-full md:w-1/4 lg:w-1/6">
<div class="flex flex-col w-full md:w-1/4 lg:w-1/6" v-if="!isSearchPage">
<MyGroupsCard />
<MyFriendsCard />
<MySkillsCard />
@ -19,11 +19,12 @@
<script setup>
import { useStore } from "vuex"
import { onMounted, provide, readonly, ref, watch } from "vue"
import SocialNetworkWall from "./SocialWall.vue"
import { onMounted, provide, computed, readonly, ref, watch } from "vue"
import { useRoute } from "vue-router"
import SocialSideMenu from "../../components/social/SocialSideMenu.vue"
import SocialWall from "./SocialWall.vue"
import SocialSearch from "./SocialSearch.vue"
import UserProfileCard from "../../components/social/UserProfileCard.vue"
import SocialSideMenu from "../../components/social/SocialSideMenu.vue"
import MyGroupsCard from "../../components/social/MyGroupsCard.vue"
import MyFriendsCard from "../../components/social/MyFriendsCard.vue"
import MySkillsCard from "../../components/social/MySkillsCard.vue"
@ -40,4 +41,10 @@ provide("group-info", groupInfo)
provide("is-group", isGroup)
onMounted(loadUser)
const isSearchPage = computed(() => route.path.includes('/social/search'))
const currentComponent = computed(() => {
return isSearchPage.value ? SocialSearch : SocialWall
})
</script>

@ -0,0 +1,292 @@
<template>
<div class="social-search p-2">
{{ query.value }}
<BaseCard class="mb-2">
<template #header>
<div class="px-4 py-2 -mb-2 bg-gray-15">
<h2 class="text-h5">{{ headerTitle }}</h2>
</div>
</template>
<div class="flex flex-col items-end">
<div class="w-full flex justify-between items-center mb-2">
<label for="search-query" class="mr-2">{{ t('Users, Groups') }}</label>
<BaseInputText
id="search-query"
v-model="query"
class="flex-grow"
label=""/>
</div>
<div class="w-full flex justify-between items-center mb-4">
<label for="search-type" class="mr-2">{{ t('Type') }}</label>
<BaseSelect
id="search-type"
v-model="searchType"
:options="searchOptions"
optionLabel="name"
optionValue="code"
class="flex-grow"
label=""/>
</div>
<BaseButton
label="Search"
icon="search"
@click="performSearch"
type="secondary"
class="self-end"
/>
</div>
</BaseCard>
<BaseCard v-if="users.length" class="mb-2">
<template #header>
<div class="px-4 py-2 -mb-2 bg-gray-15">
<h2 class="text-h5">{{ t('Users') }}</h2>
</div>
</template>
<ul>
<li v-for="user in users" :key="user.id" class="flex items-center justify-between p-2 border-b-2">
<div class="flex items-center">
<img :src="user.avatar" class="w-16 h-16 rounded-full mr-4">
<span>{{ user.name }}</span>
<span v-if="user.status === 'online'" class="mdi mdi-circle green mx-2" title="Online"></span>
<span v-else class="mdi mdi-circle gray mx-2" title="Offline"></span>
<span :class="getRoleIcon(user.role)" class="mx-2"></span>
</div>
<div>
<BaseButton
v-if="user.showInvitationButton"
@click="openInvitationModal(user)"
label="Send invitation"
icon="account"
type="secondary"
class="mr-2"
/>
<BaseButton
@click="openMessageModal(user)"
label="Send message"
icon="email"
type="primary"
/>
</div>
</li>
</ul>
</BaseCard>
<BaseCard v-if="groups.length" class="mb-2">
<template #header>
<div class="px-4 py-2 -mb-2 bg-gray-15">
<h2 class="text-h5">{{ t('Groups') }}</h2>
</div>
</template>
<div class="grid md:grid-cols-2 lg:grid-cols-4 gap-4 p-4">
<div v-for="group in groups" :key="group.id" class="group-card">
<div class="group-image flex justify-center">
<img :src="group.image" class="rounded w-16 h-16"
</div>
<div class="group-info text-center">
<h3>{{ group.name }}</h3>
<p>{{ group.description }}</p>
<a :href="group.url">
<BaseButton label="See more" type="secondary" class="mt-2" icon=""/>
</a>
</div>
</div>
</div>
</BaseCard>
<!-- Invitation Modal -->
<div v-if="showInvitationModal" class="invitation-modal-overlay" @click.self="closeInvitationModal">
<div class="invitation-modal">
<div class="invitation-modal-header">
<h3>Send invitation</h3>
<button class="close-button" @click="closeInvitationModal"></button>
</div>
<textarea class="invitation-modal-textarea" placeholder="Add a personal message" v-model="invitationMessage"></textarea>
<button class="invitation-modal-send" @click="sendInvitation">Send message</button>
</div>
</div>
<!-- Message Modal -->
<div v-if="showMessageModal" class="message-modal-overlay" @click.self="closeMessageModal">
<div class="message-modal">
<div class="message-modal-header">
<h3>{{ t('Send message') }}</h3>
<button class="message-modal-close" @click="closeMessageModal"></button>
</div>
<div class="message-modal-body">
<div class="message-user-info">
<img :src="selectedUser.avatar" class="message-user-avatar" alt="User avatar">
<span class="message-user-name">{{ selectedUser.name }}</span>
</div>
<input type="text" class="message-modal-input" placeholder="{{ t('Subject') }}" v-model="messageSubject">
<textarea class="message-modal-textarea" placeholder="{{ t('Message') }}" v-model="messageContent"></textarea>
<button class="message-modal-send" @click="sendMessage">{{ t('Send message') }}</button>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { nextTick, ref, computed } from "vue"
import BaseCard from "../../components/basecomponents/BaseCard.vue"
import BaseInputText from "../../components/basecomponents/BaseInputText.vue"
import BaseSelect from "../../components/basecomponents/BaseSelect.vue"
import BaseButton from "../../components/basecomponents/BaseButton.vue"
import { useI18n } from "vue-i18n"
import { useNotification } from "../../composables/notification"
import { useSocialInfo } from "../../composables/useSocialInfo"
const query = ref('')
const searchType = ref('user')
const searchPerformed = ref(false)
const { t } = useI18n()
const notification = useNotification()
const selectedUser = ref(null)
const showInvitationModal = ref(false)
const showMessageModal = ref(false)
const messageSubject = ref('')
const messageContent = ref('')
const invitationMessage = ref('')
const { user, groupInfo, isGroup, loadGroup, isLoading } = useSocialInfo()
const searchOptions = [
{ name: 'User', code: 'user' },
{ name: 'Group', code: 'group' }
]
const users = ref([])
const groups = ref([])
const getRoleIcon = (role) => {
switch(role) {
case 'student':
return 'mdi mdi-school'
case 'teacher':
return 'mdi mdi-account-outline'
case 'admin':
return 'mdi mdi-briefcase-check'
default:
return 'mdi mdi-account'
}
}
const headerTitle = computed(() => {
return searchPerformed.value ? `${t('Results and feedback')} "${query.value}"` : t('Search')
})
const performSearch = async () => {
try {
if (query.value.trim() === '') {
notification.showWarningNotification('Please enter a search term.')
return
}
searchPerformed.value = true
await nextTick()
const response = await fetch(`/social-network/search?query=${query.value}&type=${searchType.value}`)
const data = await response.json()
if (!response.ok) {
throw new Error(data.message || 'Server response error')
}
if (searchType.value === 'user') {
users.value = data.results.map(item => ({
...item,
showInvitationButton: ![3, 4].includes(item.relationType) && item.id !== user.value.id
}))
groups.value = []
} else if (searchType.value === 'group') {
groups.value = data.results
users.value = []
}
} catch (error) {
console.error('There has been a problem with your fetch operation:', error)
}
}
const openMessageModal = (user) => {
selectedUser.value = user
showMessageModal.value = true
}
const closeMessageModal = () => {
showMessageModal.value = false
messageSubject.value = ''
messageContent.value = ''
}
const openInvitationModal = (user) => {
selectedUser.value = user
showInvitationModal.value = true
}
const closeInvitationModal = () => {
showInvitationModal.value = false
invitationMessage.value = ''
}
const sendInvitation = async () => {
if (!selectedUser.value) {
notification.showErrorNotification('No user selected.')
return
}
const invitationData = {
userId: user.value.id,
targetUserId: selectedUser.value.id,
action: 'send_invitation',
subject: '',
content: invitationMessage.value
}
try {
const response = await fetch('/social-network/user-action', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(invitationData)
})
const result = await response.json()
if (result.success) {
notification.showSuccessNotification('Invitation sent successfully.')
users.value = users.value.filter((user) => user.id !== selectedUser.value.id)
selectedUser.value = null
} else {
notification.showErrorNotification('Failed to send invitation.')
}
} catch (error) {
notification.showErrorNotification('An error occurred while sending the invitation.')
console.error('Error sending invitation:', error)
}
showInvitationModal.value = false
invitationMessage.value = ''
}
const sendMessage = async () => {
if (!selectedUser.value) {
notification.showErrorNotification('No user selected.')
return
}
const messageData = {
userId: user.value.id,
targetUserId: selectedUser.value.id,
action: 'send_message',
subject: messageSubject.value,
content: messageContent.value
}
try {
const response = await fetch('/social-network/user-action', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(messageData)
})
const result = await response.json()
if (result.success) {
notification.showSuccessNotification('Message sent successfully.')
} else {
notification.showErrorNotification('Failed to send message.')
}
} catch (error) {
notification.showErrorNotification('An error occurred while sending the message.')
console.error('Error sending message:', error)
}
closeMessageModal()
}
</script>

@ -1,40 +1,74 @@
<template>
<div class="social-group-show">
<div v-if="!isLoading && groupInfo.isMember" class="social-group-show">
<div class="group-header">
<h1 class="group-title">mi grupo 0002</h1>
<p class="group-description">test</p>
<h1 class="group-title">{{ groupInfo?.title || '...' }}</h1>
<p class="group-description">{{ groupInfo?.description }}</p>
</div>
<ul class="tabs">
<li :class="{ active: activeTab === 'discussions' }" @click="activeTab = 'discussions'">Discussions</li>
<li :class="{ active: activeTab === 'members' }" @click="activeTab = 'members'">Members</li>
<li :class="{ active: activeTab === 'discussions' }" @click="activeTab = 'discussions'">{{ t('Discussions') }}</li>
<li :class="{ active: activeTab === 'members' }" @click="activeTab = 'members'">{{ t('Members') }}</li>
</ul>
<div class="tab-content">
<GroupDiscussions v-if="activeTab === 'discussions'" :group-id="groupId" />
<GroupMembers v-if="activeTab === 'members'" :group-id="groupId" />
<GroupDiscussions v-if="activeTab === 'discussions'" :group-id="groupInfo.id" />
<GroupMembers v-if="activeTab === 'members'" :group-id="groupInfo.id" />
</div>
</div>
<div v-if="!isLoading && !groupInfo.isMember" class="text-center">
<div class="group-header">
<h1 class="group-title">{{ groupInfo?.title || '...' }}</h1>
<p class="group-description">{{ groupInfo?.description }}</p>
</div>
<p v-if="groupInfo.visibility === 2">{{ t('This is a closed group.') }}</p>
<p v-if="groupInfo.role === 3">{{ t('You already sent an invitation') }}</p>
<p v-else>{{ t('Join this group to see the content.') }}</p>
<BaseButton
v-if="groupInfo.visibility === 1 && groupInfo.role !== 3"
:label="t('Join to group')"
type="primary"
class="mt-4"
@click="joinGroup"
icon="mdi-account-multiple-plus"
/>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import axios from 'axios'
import GroupDiscussions from "../../components/usergroup/GroupDiscussions.vue"
import GroupMembers from "../../components/usergroup/GroupMembers.vue"
import { useI18n } from "vue-i18n"
import { useSocialInfo } from "../../composables/useSocialInfo"
import axios from "axios"
import BaseButton from "../../components/basecomponents/BaseButton.vue"
const { t } = useI18n()
const route = useRoute()
const activeTab = ref('discussions')
const groupId = ref(route.params.group_id)
const group = ref(null)
onMounted(async () => {
if (groupId.value) {
try {
const response = await axios.get(`/api/usergroup/${groupId.value}`)
group.value = response.data
} catch (error) {
console.error('Error fetching group details:', error)
const { user, groupInfo, isGroup, loadGroup, isLoading } = useSocialInfo();
const joinGroup = async () => {
try {
const response = await axios.post('/social-network/group-action', {
userId: user.value.id,
groupId: groupInfo.value.id,
action: 'join'
});
if (response.data.success) {
await loadGroup(groupInfo.value.id);
}
} catch (error) {
console.error('Error joining the group:', error);
}
};
onMounted(async () => {
if (route.params.group_id) {
await loadGroup(route.params.group_id);
}
})
});
</script>

@ -0,0 +1,114 @@
<template>
<BaseButton
label="Try and find some friends"
icon="search"
type="success"
size="normal"
@click="goToSearch"
/>
<div>
<InvitationList
:invitations="receivedInvitations"
title="Invitations Received"
@accept="acceptInvitation"
@deny="denyInvitation"
/>
<InvitationList
:invitations="sentInvitations"
title="Invitations Sent"
/>
<InvitationList
:invitations="pendingInvitations"
title="Pending Group Invitations"
@accept="acceptGroupInvitation"
@deny="denyGroupInvitation"
/>
</div>
</template>
<script setup>
import axios from 'axios'
import { inject, onMounted, ref, watchEffect } from "vue"
import InvitationList from "../../components/userreluser/InvitationList.vue"
import BaseButton from "../../components/basecomponents/BaseButton.vue"
import { useRouter } from "vue-router"
const receivedInvitations = ref([])
const sentInvitations = ref([])
const pendingInvitations = ref([])
const router = useRouter()
const user = inject('social-user')
const isCurrentUser = inject('is-current-user')
watchEffect(() => {
if (user.value && user.value.id) {
fetchInvitations(user.value.id)
}
})
const fetchInvitations = async (userId) => {
if (!userId) return
try {
const response = await axios.get(`/social-network/invitations/${userId}`)
console.log('Invitations :::', response.data)
receivedInvitations.value = response.data.receivedInvitations
sentInvitations.value = response.data.sentInvitations
pendingInvitations.value = response.data.pendingGroupInvitations
} catch (error) {
console.error('Error fetching invitations:', error)
}
}
function goToSearch() {
router.push({ name: 'SocialSearch' })
}
const acceptInvitation = async (invitationId) => {
const invitation = receivedInvitations.value.find(invite => invite.id === invitationId)
if (!invitation) return
console.log('Invitation object:', invitation)
const data = {
userId: user.value.id,
targetUserId: invitation.itemId,
action: 'add_friend',
is_my_friend: true,
}
try {
const response = await axios.post('/social-network/user-action', data)
if (response.data.success) {
console.log('Invitation accepted successfully')
fetchInvitations(user.value.id)
} else {
console.error('Failed to accept invitation')
}
} catch (error) {
console.error('Error accepting invitation:', error)
}
}
const denyInvitation = async (invitationId) => {
const invitation = receivedInvitations.value.find(invite => invite.id === invitationId)
if (!invitation) return
const data = {
userId: user.value.id,
targetUserId: invitation.itemId,
action: 'deny_friend',
}
try {
const response = await axios.post('/social-network/user-action', data)
if (response.data.success) {
console.log('Invitation denied successfully')
await fetchInvitations(user.value.id)
} else {
console.error('Failed to deny invitation')
}
} catch (error) {
console.error('Error denying invitation:', error)
}
}
const acceptGroupInvitation = (groupId) => {
console.log(`Accepted group invitation with ID: ${groupId}`)
}
const denyGroupInvitation = (groupId) => {
console.log(`Denied group invitation with ID: ${groupId}`)
}
</script>

@ -8,10 +8,13 @@ namespace Chamilo\CoreBundle\Controller;
use Chamilo\CoreBundle\Entity\ExtraField;
use Chamilo\CoreBundle\Entity\User;
use Chamilo\CoreBundle\Entity\Usergroup;
use Chamilo\CoreBundle\Entity\UserRelUser;
use Chamilo\CoreBundle\Repository\ExtraFieldOptionsRepository;
use Chamilo\CoreBundle\Repository\ExtraFieldRepository;
use Chamilo\CoreBundle\Repository\LanguageRepository;
use Chamilo\CoreBundle\Repository\LegalRepository;
use Chamilo\CoreBundle\Repository\MessageRepository;
use Chamilo\CoreBundle\Repository\Node\IllustrationRepository;
use Chamilo\CoreBundle\Repository\Node\UsergroupRepository;
use Chamilo\CoreBundle\Repository\Node\UserRepository;
@ -20,8 +23,10 @@ use Chamilo\CoreBundle\Serializer\UserToJsonNormalizer;
use Chamilo\CoreBundle\Settings\SettingsManager;
use Chamilo\CourseBundle\Repository\CForumThreadRepository;
use DateTime;
use Doctrine\ORM\EntityManagerInterface;
use Exception;
use ExtraFieldValue;
use MessageManager;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
@ -276,8 +281,13 @@ class SocialController extends AbstractController
}
#[Route('/invite-friends/{userId}/{groupId}', name: 'chamilo_core_social_invite_friends')]
public function inviteFriends(int $userId, int $groupId, UserRepository $userRepository, UsergroupRepository $usergroupRepository, IllustrationRepository $illustrationRepository): JsonResponse
{
public function inviteFriends(
int $userId,
int $groupId,
UserRepository $userRepository,
UsergroupRepository $usergroupRepository,
IllustrationRepository $illustrationRepository
): JsonResponse {
$user = $userRepository->find($userId);
if (!$user) {
return $this->json(['error' => 'User not found'], Response::HTTP_NOT_FOUND);
@ -440,6 +450,260 @@ class SocialController extends AbstractController
return $extraFieldsFormatted;
}
#[Route('/invitations/{userId}', name: 'chamilo_core_social_invitations')]
public function getInvitations(
int $userId,
MessageRepository $messageRepository,
UsergroupRepository $usergroupRepository,
UserRepository $userRepository
): JsonResponse {
$user = $this->getUser();
if ($userId !== $user->getId()) {
return $this->json(['error' => 'Unauthorized'], Response::HTTP_UNAUTHORIZED);
}
$receivedMessages = $messageRepository->findReceivedInvitationsByUser($user);
$receivedInvitations = [];
foreach ($receivedMessages as $message) {
$sender = $message->getSender();
$receivedInvitations[] = [
'id' => $message->getId(),
'itemId' => $sender->getId(),
'itemName' => $sender->getFirstName() . ' ' . $sender->getLastName(),
'itemPicture' => $userRepository->getUserPicture($sender->getId()),
'content' => $message->getContent(),
'date' => $message->getSendDate()->format('Y-m-d H:i:s'),
'canAccept' => true,
'canDeny' => true,
];
}
$sentMessages = $messageRepository->findSentInvitationsByUser($user);
$sentInvitations = [];
foreach ($sentMessages as $message) {
foreach ($message->getReceivers() as $receiver) {
$receiverUser = $receiver->getReceiver();
$sentInvitations[] = [
'id' => $message->getId(),
'itemId' => $receiverUser->getId(),
'itemName' => $receiverUser->getFirstName() . ' ' . $receiverUser->getLastName(),
'itemPicture' => $userRepository->getUserPicture($receiverUser->getId()),
'content' => $message->getContent(),
'date' => $message->getSendDate()->format('Y-m-d H:i:s'),
'canAccept' => false,
'canDeny' => false,
];
}
}
$pendingGroupInvitations = [];
$pendingGroups = $usergroupRepository->getGroupsByUser($userId, Usergroup::GROUP_USER_PERMISSION_PENDING_INVITATION);
foreach ($pendingGroups as $group) {
$pendingGroupInvitations[] = [
'id' => $group->getId(),
'itemId' => $group->getId(),
'itemName' => $group->getTitle(),
'itemPicture' => $usergroupRepository->getUsergroupPicture($group->getId()),
'content' => $group->getDescription(),
'date' => $group->getCreatedAt()->format('Y-m-d H:i:s'),
'canAccept' => true,
'canDeny' => true,
];
}
return $this->json([
'receivedInvitations' => $receivedInvitations,
'sentInvitations' => $sentInvitations,
'pendingGroupInvitations' => $pendingGroupInvitations,
]);
}
#[Route('/search', name: 'chamilo_core_social_search')]
public function search(
Request $request,
UserRepository $userRepository,
UsergroupRepository $usergroupRepository,
TrackEOnlineRepository $trackOnlineRepository
): JsonResponse {
$query = $request->query->get('query', '');
$type = $request->query->get('type', 'user');
$from = $request->query->getInt('from', 0);
$numberOfItems = $request->query->getInt('number_of_items', 1000);
$formattedResults = [];
if ($type === 'user') {
/* @var User $user */
$user = $this->getUser();
$results = $userRepository->searchUsersByTags($query, $user->getId(), 0, $from, $numberOfItems);
foreach ($results as $item) {
$isUserOnline = $trackOnlineRepository->isUserOnline($item['id']);
$relation = $userRepository->getUserRelationWithType($user->getId(), $item['id']);
$formattedResults[] = [
'id' => $item['id'],
'name' => $item['firstname'] . ' ' . $item['lastname'],
'avatar' => $userRepository->getUserPicture($item['id']),
'role' => $item['status'] === 5 ? 'student' : 'teacher',
'status' => $isUserOnline ? 'online' : 'offline',
'url' => '/social?id=' . $item['id'],
'relationType' => $relation['relationType'] ?? null,
];
}
} elseif ($type === 'group') {
// Perform group search
$results = $usergroupRepository->searchGroupsByTags($query, $from, $numberOfItems);
foreach ($results as $item) {
$formattedResults[] = [
'id' => $item['id'],
'name' => $item['title'],
'description' => $item['description'] ?? '',
'image' => $usergroupRepository->getUsergroupPicture($item['id']),
'url' => '/resources/usergroups/show/' . $item['id'],
];
}
}
return $this->json(['results' => $formattedResults]);
}
#[Route('/group-details/{groupId}', name: 'chamilo_core_social_group_details')]
public function groupDetails(
int $groupId,
UsergroupRepository $usergroupRepository,
TrackEOnlineRepository $trackOnlineRepository
): JsonResponse {
/* @var User $user */
$user = $this->getUser();
if (!$user) {
return $this->json(['error' => 'User not authenticated'], Response::HTTP_UNAUTHORIZED);
}
/* @var Usergroup $group */
$group = $usergroupRepository->find($groupId);
if (!$group) {
return $this->json(['error' => 'Group not found'], Response::HTTP_NOT_FOUND);
}
$isMember = $usergroupRepository->isGroupMember($groupId, $user);
$role = $usergroupRepository->getUserGroupRole($groupId, $user->getId());
$isUserOnline = $trackOnlineRepository->isUserOnline($user->getId());
$isModerator = $usergroupRepository->isGroupModerator($groupId, $user->getId());
$groupDetails = [
'id' => $group->getId(),
'title' => $group->getTitle(),
'description' => $group->getDescription(),
'image' => $usergroupRepository->getUsergroupPicture($group->getId()),
'isMember' => $isMember,
'isModerator' => $isModerator,
'role' => $role,
'isUserOnline' => $isUserOnline,
'visibility' => (int) $group->getVisibility(),
];
return $this->json($groupDetails);
}
#[Route('/group-action', name: 'chamilo_core_social_group_action')]
public function groupAction(Request $request, UsergroupRepository $usergroupRepository, EntityManagerInterface $em): JsonResponse
{
$data = json_decode($request->getContent(), true);
$userId = $data['userId'] ?? null;
$groupId = $data['groupId'] ?? null;
$action = $data['action'] ?? null;
if (!$userId || !$groupId || !$action) {
return $this->json(['error' => 'Missing parameters'], Response::HTTP_BAD_REQUEST);
}
try {
switch ($action) {
case 'join':
$usergroupRepository->addUserToGroup($userId, $groupId);
break;
case 'leave':
$usergroupRepository->removeUserFromGroup($userId, $groupId);
break;
default:
return $this->json(['error' => 'Invalid action'], Response::HTTP_BAD_REQUEST);
}
$em->flush();
return $this->json(['success' => 'Action completed successfully']);
} catch (\Exception $e) {
return $this->json(['error' => $e->getMessage()], Response::HTTP_INTERNAL_SERVER_ERROR);
}
}
#[Route('/user-action', name: 'chamilo_core_social_user_action')]
public function userAction(
Request $request,
UserRepository $userRepository,
MessageRepository $messageRepository,
EntityManagerInterface $em
): JsonResponse {
$data = json_decode($request->getContent(), true);
$userId = $data['userId'] ?? null;
$targetUserId = $data['targetUserId'] ?? null;
$action = $data['action'] ?? null;
$isMyFriend = $data['is_my_friend'] ?? false;
$subject = $data['subject'] ?? '';
$content = $data['content'] ?? '';
if (!$userId || !$targetUserId || !$action) {
return $this->json(['error' => 'Missing parameters'], Response::HTTP_BAD_REQUEST);
}
$userSender = $userRepository->find($userId);
$userReceiver = $userRepository->find($targetUserId);
if (null === $userSender || null === $userReceiver) {
return $this->json(['error' => 'User not found'], Response::HTTP_NOT_FOUND);
}
try {
switch ($action) {
case 'send_invitation':
$result = $messageRepository->sendInvitationToFriend($userSender, $userReceiver, $subject, $content);
if (!$result) {
return $this->json(['error' => 'Invitation already exists or could not be sent'], Response::HTTP_BAD_REQUEST);
}
break;
case 'send_message':
$result = MessageManager::send_message($targetUserId, $subject, $content);
break;
case 'add_friend':
$relationType = $isMyFriend ? UserRelUser::USER_RELATION_TYPE_FRIEND : UserRelUser::USER_UNKNOWN;
$userRepository->relateUsers($userSender, $userReceiver, $relationType);
$userRepository->relateUsers($userReceiver, $userSender, $relationType);
$messageRepository->invitationAccepted($userSender, $userReceiver);
break;
case 'deny_friend':
$messageRepository->invitationDenied($userSender, $userReceiver);
break;
default:
return $this->json(['error' => 'Invalid action'], Response::HTTP_BAD_REQUEST);
}
$em->flush();
return $this->json(['success' => 'Action completed successfully']);
} catch (\Exception $e) {
return $this->json(['error' => $e->getMessage()], Response::HTTP_INTERNAL_SERVER_ERROR);
}
}
private function checkUserStatus(int $userId, UserRepository $userRepository): bool
{
$userStatus = $userRepository->getExtraUserDataByField($userId, 'user_chat_status');

@ -98,6 +98,9 @@ class Usergroup extends AbstractResource implements ResourceInterface, ResourceI
public const GROUP_USER_PERMISSION_ANONYMOUS = 6; // An anonymous user, not part of the group
public const GROUP_USER_PERMISSION_HRM = 7; // A human resource manager
public const GROUP_PERMISSION_OPEN = 1;
public const GROUP_PERMISSION_CLOSED = 2;
#[ORM\Column(name: 'id', type: 'integer', nullable: false)]
#[ORM\Id]
#[ORM\GeneratedValue]

@ -7,6 +7,7 @@ declare(strict_types=1);
namespace Chamilo\CoreBundle\Repository;
use Chamilo\CoreBundle\Entity\Message;
use Chamilo\CoreBundle\Entity\MessageRelUser;
use Chamilo\CoreBundle\Entity\User;
use Chamilo\CoreBundle\Traits\Repository\RepositoryQueryBuilderTrait;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
@ -82,4 +83,147 @@ class MessageRepository extends ServiceEntityRepository
return $qb->getQuery()->getResult();
}
public function findReceivedInvitationsByUser(User $user): array
{
return $this->createQueryBuilder('m')
->join('m.receivers', 'mr')
->where('mr.receiver = :user')
->andWhere('m.msgType = :msgType')
->andWhere('m.status = :status')
->setParameters([
'user' => $user,
'msgType' => Message::MESSAGE_TYPE_INVITATION,
'status' => Message::MESSAGE_STATUS_INVITATION_PENDING,
])
->getQuery()
->getResult();
}
public function findSentInvitationsByUser(User $user): array
{
return $this->createQueryBuilder('m')
->where('m.sender = :user')
->andWhere('m.msgType = :msgType')
->andWhere('m.status = :status')
->setParameters([
'user' => $user,
'msgType' => Message::MESSAGE_TYPE_INVITATION,
'status' => Message::MESSAGE_STATUS_INVITATION_PENDING,
])
->getQuery()
->getResult();
}
public function sendInvitationToFriend(User $userSender, User $userReceiver, string $messageTitle, string $messageContent): bool
{
$existingInvitations = $this->findSentInvitationsByUserAndStatus($userSender, $userReceiver, [
Message::MESSAGE_STATUS_INVITATION_PENDING,
Message::MESSAGE_STATUS_INVITATION_ACCEPTED,
Message::MESSAGE_STATUS_INVITATION_DENIED
]);
if (count($existingInvitations) > 0) {
// Invitation already exists
return false;
}
$message = new Message();
$message->setSender($userSender);
$message->setMsgType(Message::MESSAGE_TYPE_INVITATION);
$message->setStatus(Message::MESSAGE_STATUS_INVITATION_PENDING);
$message->setSendDate(new \DateTime());
$message->setTitle($messageTitle);
$message->setContent(nl2br($messageContent));
$messageRelUser = new MessageRelUser();
$messageRelUser->setReceiver($userReceiver);
$messageRelUser->setReceiverType(MessageRelUser::TYPE_TO);
$message->addReceiver($messageRelUser);
$this->_em->persist($message);
$this->_em->persist($messageRelUser);
$this->_em->flush();
return true;
}
public function findSentInvitationsByUserAndStatus(User $userSender, User $userReceiver, array $statuses): array
{
$qb = $this->createQueryBuilder('m');
$qb->join('m.receivers', 'mr')
->where('m.sender = :sender')
->andWhere('mr.receiver = :receiver')
->andWhere('m.msgType = :msgType')
->andWhere($qb->expr()->in('m.status', ':statuses'))
->setParameters([
'sender' => $userSender,
'receiver' => $userReceiver,
'msgType' => Message::MESSAGE_TYPE_INVITATION,
'statuses' => $statuses,
]);
return $qb->getQuery()->getResult();
}
public function invitationAccepted(User $sender, User $receiver): bool
{
$queryBuilder = $this->_em->createQueryBuilder();
$queryBuilder->select('m')
->from(Message::class, 'm')
->where('m.sender = :sender')
->andWhere('m.status = :status')
->setParameter('sender', $sender)
->setParameter('status', Message::MESSAGE_STATUS_INVITATION_PENDING);
$messages = $queryBuilder->getQuery()->getResult();
foreach ($messages as $message) {
$messageRelUser = $this->_em->getRepository(MessageRelUser::class)->findOneBy([
'message' => $message,
'receiver' => $receiver
]);
if ($messageRelUser) {
$invitation = $messageRelUser->getMessage();
$invitation->setStatus(Message::MESSAGE_STATUS_INVITATION_ACCEPTED);
$this->_em->flush();
return true;
}
}
return false;
}
public function invitationDenied(User $sender, User $receiver): bool
{
$queryBuilder = $this->_em->createQueryBuilder();
$queryBuilder->select('m')
->from(Message::class, 'm')
->where('m.sender = :sender')
->andWhere('m.status = :status')
->setParameter('sender', $sender)
->setParameter('status', Message::MESSAGE_STATUS_INVITATION_PENDING);
$messages = $queryBuilder->getQuery()->getResult();
foreach ($messages as $message) {
$messageRelUser = $this->_em->getRepository(MessageRelUser::class)->findOneBy([
'message' => $message,
'receiver' => $receiver
]);
if ($messageRelUser) {
$this->_em->remove($messageRelUser);
$this->_em->flush();
return true;
}
}
return false;
}
}

@ -13,11 +13,13 @@ use Chamilo\CoreBundle\Entity\ExtraFieldValues;
use Chamilo\CoreBundle\Entity\Message;
use Chamilo\CoreBundle\Entity\ResourceNode;
use Chamilo\CoreBundle\Entity\Session;
use Chamilo\CoreBundle\Entity\Tag;
use Chamilo\CoreBundle\Entity\TrackELogin;
use Chamilo\CoreBundle\Entity\TrackEOnline;
use Chamilo\CoreBundle\Entity\User;
use Chamilo\CoreBundle\Entity\Usergroup;
use Chamilo\CoreBundle\Entity\UsergroupRelUser;
use Chamilo\CoreBundle\Entity\UserRelTag;
use Chamilo\CoreBundle\Entity\UserRelUser;
use Chamilo\CoreBundle\Repository\ResourceRepository;
use Datetime;
@ -37,10 +39,17 @@ use const MB_CASE_LOWER;
class UserRepository extends ResourceRepository implements PasswordUpgraderInterface
{
protected ?UserPasswordHasherInterface $hasher = null;
private $illustrationRepository;
public function __construct(ManagerRegistry $registry)
const USER_IMAGE_SIZE_SMALL = 1;
const USER_IMAGE_SIZE_MEDIUM = 2;
const USER_IMAGE_SIZE_BIG = 3;
const USER_IMAGE_SIZE_ORIGINAL = 4;
public function __construct(ManagerRegistry $registry, IllustrationRepository $illustrationRepository)
{
parent::__construct($registry, User::class);
$this->illustrationRepository = $illustrationRepository;
}
public function loadUserByIdentifier(string $identifier): ?User
@ -727,4 +736,167 @@ class UserRepository extends ResourceRepository implements PasswordUpgraderInter
return $extraData;
}
public function searchUsersByTags(
string $tag,
int $excludeUserId = null,
int $fieldId = 0,
int $from = 0,
int $number_of_items = 10,
bool $getCount = false
): array {
$qb = $this->createQueryBuilder('u');
if ($getCount) {
$qb->select('COUNT(DISTINCT u.id)');
} else {
$qb->select('DISTINCT u.id, u.username, u.firstname, u.lastname, u.email, u.pictureUri, u.status');
}
$qb->innerJoin('u.portals', 'urlRelUser')
->leftJoin(UserRelTag::class, 'uv', 'WITH', 'u = uv.user')
->leftJoin(Tag::class, 'ut', 'WITH', 'uv.tag = ut');
if ($fieldId !== 0) {
$qb->andWhere('ut.field = :fieldId')
->setParameter('fieldId', $fieldId);
}
if ($excludeUserId !== null) {
$qb->andWhere('u.id != :excludeUserId')
->setParameter('excludeUserId', $excludeUserId);
}
$qb->andWhere(
$qb->expr()->orX(
$qb->expr()->like('ut.tag', ':tag'),
$qb->expr()->like('u.firstname', ':likeTag'),
$qb->expr()->like('u.lastname', ':likeTag'),
$qb->expr()->like('u.username', ':likeTag'),
$qb->expr()->like(
$qb->expr()->concat('u.firstname', $qb->expr()->literal(' '), 'u.lastname'),
':likeTag'
),
$qb->expr()->like(
$qb->expr()->concat('u.lastname', $qb->expr()->literal(' '), 'u.firstname'),
':likeTag'
)
)
)
->setParameter('tag', $tag . '%')
->setParameter('likeTag', '%' . $tag . '%');
// Only active users and not anonymous
$qb->andWhere('u.active = :active')
->andWhere('u.status != :anonymous')
->setParameter('active', true)
->setParameter('anonymous', 6);
if (!$getCount) {
$qb->orderBy('u.username')
->setFirstResult($from)
->setMaxResults($number_of_items);
}
return $getCount ? $qb->getQuery()->getSingleScalarResult() : $qb->getQuery()->getResult();
}
public function getUserRelationWithType(int $userId, int $friendId): ?array
{
$qb = $this->createQueryBuilder('u');
$qb->select('u.id AS userId', 'u.username AS userName', 'ur.relationType', 'f.id AS friendId', 'f.username AS friendName')
->innerJoin('u.friends', 'ur')
->innerJoin('ur.friend', 'f')
->where('u.id = :userId AND f.id = :friendId')
->setParameter('userId', $userId)
->setParameter('friendId', $friendId)
->setMaxResults(1);
$result = $qb->getQuery()->getOneOrNullResult();
return $result;
}
public function relateUsers(User $user1, User $user2, int $relationType): void
{
$em = $this->getEntityManager();
$existingRelation = $em->getRepository(UserRelUser::class)->findOneBy([
'user' => $user1,
'friend' => $user2,
]);
if (!$existingRelation) {
$newRelation = new UserRelUser();
$newRelation->setUser($user1);
$newRelation->setFriend($user2);
$newRelation->setRelationType($relationType);
$em->persist($newRelation);
} else {
$existingRelation->setRelationType($relationType);
}
$existingRelationInverse = $em->getRepository(UserRelUser::class)->findOneBy([
'user' => $user2,
'friend' => $user1,
]);
if (!$existingRelationInverse) {
$newRelationInverse = new UserRelUser();
$newRelationInverse->setUser($user2);
$newRelationInverse->setFriend($user1);
$newRelationInverse->setRelationType($relationType);
$em->persist($newRelationInverse);
} else {
$existingRelationInverse->setRelationType($relationType);
}
$em->flush();
}
public function getUserPicture(
$userId,
int $size = self::USER_IMAGE_SIZE_MEDIUM,
$addRandomId = true,
) {
$user = $this->find($userId);
if (!$user) {
return '/img/icons/64/unknown.png';
}
switch ($size) {
case self::USER_IMAGE_SIZE_SMALL:
$width = 32;
break;
case self::USER_IMAGE_SIZE_MEDIUM:
$width = 64;
break;
case self::USER_IMAGE_SIZE_BIG:
$width = 128;
break;
case self::USER_IMAGE_SIZE_ORIGINAL:
default:
$width = 0;
break;
}
$url = $this->illustrationRepository->getIllustrationUrl($user);
$params = [];
if (!empty($width)) {
$params['w'] = $width;
}
if ($addRandomId) {
$params['rand'] = uniqid('u_', true);
}
$paramsToString = '';
if (!empty($params)) {
$paramsToString = '?'.http_build_query($params);
}
return $url.$paramsToString;
}
}

@ -15,9 +15,12 @@ use Exception;
class UsergroupRepository extends ResourceRepository
{
public function __construct(ManagerRegistry $registry)
private $illustrationRepository;
public function __construct(ManagerRegistry $registry, IllustrationRepository $illustrationRepository)
{
parent::__construct($registry, Usergroup::class);
$this->illustrationRepository = $illustrationRepository;
}
/**
@ -162,33 +165,80 @@ class UsergroupRepository extends ResourceRepository
Usergroup::GROUP_USER_PERMISSION_READER,
Usergroup::GROUP_USER_PERMISSION_PENDING_INVITATION,
])
->select('u.id, u.username, u.email, gu.relationType')
->select('u.id, u.username, u.email, gu.relationType, u.pictureUri')
;
return $qb->getQuery()->getResult();
$results = $qb->getQuery()->getResult();
$userRepository = $this->_em->getRepository(User::class);
foreach ($results as &$user) {
$user['pictureUri'] = $userRepository->getUserPicture($user['id']);
}
return $results;
}
public function addUserToGroup(array $userIds, int $groupId): void
public function addUserToGroup(int $userId, int $groupId, int $relationType = Usergroup::GROUP_USER_PERMISSION_READER): void
{
$group = $this->find($groupId);
if (!$group) {
throw new Exception('Group not found');
}
foreach ($userIds as $userId) {
$user = $this->_em->getRepository(User::class)->find($userId);
if ($user) {
$groupRelUser = new UsergroupRelUser();
$groupRelUser->setUsergroup($group);
$groupRelUser->setUser($user);
$groupRelUser->setRelationType(Usergroup::GROUP_USER_PERMISSION_PENDING_INVITATION);
$this->_em->persist($groupRelUser);
}
$user = $this->_em->getRepository(User::class)->find($userId);
if (!$group || !$user) {
throw new Exception('Group or User not found');
}
$existingRelation = $this->_em->getRepository(UsergroupRelUser::class)->findOneBy([
'usergroup' => $group,
'user' => $user,
]);
if (!$existingRelation) {
$existingRelation = new UsergroupRelUser();
$existingRelation->setUsergroup($group);
$existingRelation->setUser($user);
}
if ($group->getVisibility() === Usergroup::GROUP_PERMISSION_CLOSED) {
$relationType = Usergroup::GROUP_USER_PERMISSION_PENDING_INVITATION;
}
$existingRelation->setRelationType($relationType);
$this->_em->persist($existingRelation);
$this->_em->flush();
}
public function removeUserFromGroup(int $userId, int $groupId): bool
{
/* @var Usergroup $group */
$group = $this->find($groupId);
$user = $this->_em->getRepository(User::class)->find($userId);
if (!$group || !$user) {
throw new Exception('Group or User not found');
}
if (!$group->getAllowMembersToLeaveGroup()) {
throw new Exception('Members are not allowed to leave this group');
}
$relation = $this->_em->getRepository(UsergroupRelUser::class)->findOneBy([
'usergroup' => $group,
'user' => $user,
]);
if ($relation) {
$this->_em->remove($relation);
$this->_em->flush();
return true;
}
return false;
}
public function getInvitedUsersByGroup(int $groupID)
{
$qb = $this->createQueryBuilder('g')
@ -219,6 +269,99 @@ class UsergroupRepository extends ResourceRepository
return $qb->getQuery()->getResult();
}
public function searchGroupsByTags(string $tag, int $from = 0, int $number_of_items = 10, bool $getCount = false)
{
$qb = $this->createQueryBuilder('g');
if ($getCount) {
$qb->select('COUNT(g.id)');
} else {
$qb->select('g.id, g.title, g.description, g.url, g.picture');
}
if ($this->getUseMultipleUrl()) {
$urlId = $this->getCurrentAccessUrlId();
$qb->innerJoin('g.accessUrls', 'a', 'WITH', 'g.id = a.usergroup')
->andWhere('a.accessUrl = :urlId')
->setParameter('urlId', $urlId);
}
$qb->where(
$qb->expr()->orX(
$qb->expr()->like('g.title', ':tag'),
$qb->expr()->like('g.description', ':tag'),
$qb->expr()->like('g.url', ':tag')
)
)
->setParameter('tag', '%' . $tag . '%');
if (!$getCount) {
$qb->orderBy('g.title', 'ASC')
->setFirstResult($from)
->setMaxResults($number_of_items);
}
return $getCount ? $qb->getQuery()->getSingleScalarResult() : $qb->getQuery()->getResult();
}
public function getUsergroupPicture($userGroupId): string
{
$usergroup = $this->find($userGroupId);
if (!$usergroup) {
return '/img/icons/64/group_na.png';
}
$url = $this->illustrationRepository->getIllustrationUrl($usergroup);
$params['w'] = 64;
$params['rand'] = uniqid('u_', true);
$paramsToString = '?'.http_build_query($params);
return $url.$paramsToString;
}
public function isGroupMember(int $groupId, User $user): bool
{
if ($user->isSuperAdmin()) {
return true;
}
$userRole = $this->getUserGroupRole($groupId, $user->getId());
$allowedRoles = [
Usergroup::GROUP_USER_PERMISSION_ADMIN,
Usergroup::GROUP_USER_PERMISSION_MODERATOR,
Usergroup::GROUP_USER_PERMISSION_READER,
Usergroup::GROUP_USER_PERMISSION_HRM,
];
return in_array($userRole, $allowedRoles, true);
}
public function getUserGroupRole(int $groupId, int $userId): ?int
{
$qb = $this->createQueryBuilder('g');
$qb->innerJoin('g.users', 'gu')
->where('g.id = :groupId AND gu.user = :userId')
->setParameter('groupId', $groupId)
->setParameter('userId', $userId)
->select('gu.relationType');
$result = $qb->getQuery()->getOneOrNullResult();
return $result ? $result['relationType'] : null;
}
public function isGroupModerator(int $groupId, int $userId): bool
{
$relationType = $this->getUserGroupRole($groupId, $userId);
return in_array($relationType, [
Usergroup::GROUP_USER_PERMISSION_ADMIN,
Usergroup::GROUP_USER_PERMISSION_MODERATOR,
]);
}
/**
* Determines whether to use the multi-URL feature.
*

Loading…
Cancel
Save