diff --git a/client/src/app/domain/definitions/permission.config.ts b/client/src/app/domain/definitions/permission.config.ts index d4e77a95b0..d8a46f3499 100644 --- a/client/src/app/domain/definitions/permission.config.ts +++ b/client/src/app/domain/definitions/permission.config.ts @@ -7,6 +7,7 @@ export type PermissionsMap = { [key in Permission]?: Permission[] }; export interface DisplayPermission { display_name: string; help_text?: string; + anon_allowed?: boolean; value: Permission; } @@ -24,6 +25,7 @@ export const PERMISSIONS: AppPermission[] = [ help_text: _( `Can see the Autopilot menu item with all content for which appropriate permissions are set.` ), + anon_allowed: true, value: Permission.meetingCanSeeAutopilot }, { @@ -31,6 +33,7 @@ export const PERMISSIONS: AppPermission[] = [ help_text: _( `Can see the Projector menu item and all projectors (in the Autopilot as well as in the Projector menu item)` ), + anon_allowed: true, value: Permission.projectorCanSee }, { @@ -46,11 +49,13 @@ export const PERMISSIONS: AppPermission[] = [ { display_name: _(`Can see agenda`), help_text: _(`Can see the Agenda menu item and all public topics in the agenda.`), + anon_allowed: true, value: Permission.agendaItemCanSee }, { display_name: _(`Can see internal items and time scheduling of agenda`), help_text: _(`Can see all internal topics, schedules and comments.`), + anon_allowed: true, value: Permission.agendaItemCanSeeInternal }, { @@ -63,6 +68,7 @@ export const PERMISSIONS: AppPermission[] = [ { display_name: _(`Can see list of speakers`), help_text: _(`Can see all lists of speakers`), + anon_allowed: true, value: Permission.listOfSpeakersCanSee }, { @@ -74,7 +80,7 @@ export const PERMISSIONS: AppPermission[] = [ }, { display_name: _(`Can put oneself on the list of speakers`), - help_text: _(`Is allowed to add himself/herself to the list of speakers. + help_text: _(`Is allowed to add himself/herself to the list of speakers. Note: Optional combination of requests to speak with presence status is possible. ( > [Settings] > [List of speakers] > [General] )`), @@ -88,6 +94,7 @@ Optional combination of requests to speak with presence status is possible. ( > { display_name: _(`Can see moderation notes`), help_text: _(`Can see all moderation notes in each list of speakers.`), + anon_allowed: true, value: Permission.agendaItemCanSeeModeratorNotes }, { @@ -105,6 +112,7 @@ Optional combination of requests to speak with presence status is possible. ( > help_text: _( `Can see the Motions menu item and all motions unless they are limited by access restrictions in the workflow.` ), + anon_allowed: true, value: Permission.motionCanSee }, { @@ -114,6 +122,7 @@ Optional combination of requests to speak with presence status is possible. ( > Tip: Cross-check desired visibility of motions with test delegate account. ` ), + anon_allowed: true, value: Permission.motionCanSeeInternal }, { @@ -133,10 +142,10 @@ Tip: Cross-check desired visibility of motions with test delegate account. ` { display_name: _(`Can forward motions`), help_text: _( - `Can forward motions to other meetings within the OpenSlides instance. + `Can forward motions to other meetings within the OpenSlides instance. Further requirements: -1. forwarding hierarchy must be set at the organizational level in the committee. +1. forwarding hierarchy must be set at the organizational level in the committee. 2. target meeting must be created. 3. forwarding must be activated in the workflow in the state.` ), @@ -176,10 +185,11 @@ Further requirements: { display_name: _(`Can see elections`), help_text: _( - `Can see the menu item Elections, including the list of candidates and results. + `Can see the menu item Elections, including the list of candidates and results. Note: The right to vote is defined directly in the ballot.` ), + anon_allowed: true, value: Permission.assignmentCanSee }, { @@ -191,7 +201,7 @@ Note: The right to vote is defined directly in the ballot.` }, { display_name: _(`Can nominate another participant`), - help_text: _(`Can nominate other participants as candidates. + help_text: _(`Can nominate other participants as candidates. Requires group permission: [Can see participants]`), value: Permission.assignmentCanNominateOther @@ -209,15 +219,17 @@ Requires group permission: [Can see participants]`), { display_name: _(`Can see participants`), help_text: _( - `Can see the menu item Participants and therefore the following data from all participants: -Personal data: Name, pronoun, gender. + `Can see the menu item Participants and therefore the following data from all participants: +Personal data: Name, pronoun, gender. Meeting specific information: Structure level, Group, Participant number, About me, Presence status.` ), + anon_allowed: true, value: Permission.userCanSee }, { display_name: _(`Can see sensitive data`), help_text: _(`Can see email, username and SSO identification of all participants.`), + anon_allowed: true, value: Permission.userCanSeeSensitiveData }, { @@ -245,6 +257,7 @@ Meeting specific information: Structure level, Group, Participant number, About help_text: _(`Can see the Files menu item and all shared folders and files. Note: Sharing of folders and files may be restricted by group assignment.`), + anon_allowed: true, value: Permission.mediafileCanSee }, { @@ -267,6 +280,7 @@ Note: Sharing of folders and files may be restricted by group assignment.`), { display_name: _(`Can see the front page`), help_text: _(`Can see the Home menu item.`), + anon_allowed: true, value: Permission.meetingCanSeeFrontpage }, { @@ -274,15 +288,17 @@ Note: Sharing of folders and files may be restricted by group assignment.`), help_text: _( `Can see the livestream if there is a livestream URL entered in > [Settings] > [Livestream].` ), + anon_allowed: true, value: Permission.meetingCanSeeLivestream }, { display_name: _(`Can see history`), help_text: _( - `Can see the History menu item with the history of processing timestamps for motions, elections and participants. + `Can see the History menu item with the history of processing timestamps for motions, elections and participants. Note: For privacy reasons, it is recommended to limit the rights to view the History significantly.` ), + anon_allowed: true, value: Permission.meetingCanSeeHistory }, { diff --git a/client/src/app/domain/models/meetings/meeting.ts b/client/src/app/domain/models/meetings/meeting.ts index 3734056266..e52c00328c 100644 --- a/client/src/app/domain/models/meetings/meeting.ts +++ b/client/src/app/domain/models/meetings/meeting.ts @@ -253,6 +253,7 @@ export class Meeting extends BaseModel { public default_group_id!: Id; // group/default_group_for_meeting_id; public admin_group_id!: Id; // group/admin_group_for_meeting_id; + public anonymous_group_id!: Id; // group/anonymous_group_for_meeting_id; public list_of_speakers_countdown_id: Id; // projector_countdown/used_as_list_of_speakers_meeting_id; public poll_countdown_id: Id; // projector_countdown/used_as_poll_countdown_meeting_id; @@ -513,7 +514,8 @@ export class Meeting extends BaseModel { `default_projector_motion_poll_ids`, `default_projector_poll_ids`, `default_group_id`, - `admin_group_id` + `admin_group_id`, + `anonymous_group_id` ]; } export interface Meeting diff --git a/client/src/app/domain/models/users/group.ts b/client/src/app/domain/models/users/group.ts index 1843ba71c5..f23b0f058b 100644 --- a/client/src/app/domain/models/users/group.ts +++ b/client/src/app/domain/models/users/group.ts @@ -17,6 +17,7 @@ export class Group extends BaseModel { public meeting_user_ids!: Id[]; // (meeting_user/group_ids)[]; public default_group_for_meeting_id!: Id; // meeting/default_group_id; + public anonymous_group_for_meeting_id!: Id; // meeting/admin_group_id; public admin_group_for_meeting_id!: Id; // meeting/admin_group_id; public meeting_mediafile_access_group_ids!: Id[]; // (mediafile/access_group_ids)[]; public meeting_mediafile_inherited_access_group_ids!: Id[]; // (mediafile/inherited_access_group_ids)[]; @@ -34,6 +35,10 @@ export class Group extends BaseModel { return !!this.admin_group_for_meeting_id; } + public get isAnonymousGroup(): boolean { + return !!this.anonymous_group_for_meeting_id; + } + public get isDefaultGroup(): boolean { return !!this.default_group_for_meeting_id; } @@ -50,6 +55,7 @@ export class Group extends BaseModel { `weight`, `meeting_user_ids`, `default_group_for_meeting_id`, + `anonymous_group_for_meeting_id`, `admin_group_for_meeting_id`, `meeting_mediafile_access_group_ids`, `meeting_mediafile_inherited_access_group_ids`, diff --git a/client/src/app/gateways/repositories/meeting-repository.service.ts b/client/src/app/gateways/repositories/meeting-repository.service.ts index 254df4d5bc..0b04cf6021 100644 --- a/client/src/app/gateways/repositories/meeting-repository.service.ts +++ b/client/src/app/gateways/repositories/meeting-repository.service.ts @@ -73,7 +73,8 @@ export class MeetingRepositoryService extends BaseRepository = [ `default_meeting_for_committee_id`, diff --git a/client/src/app/infrastructure/definitions/relations/relations.ts b/client/src/app/infrastructure/definitions/relations/relations.ts index 60e706a88e..c0a5152007 100644 --- a/client/src/app/infrastructure/definitions/relations/relations.ts +++ b/client/src/app/infrastructure/definitions/relations/relations.ts @@ -653,6 +653,12 @@ export const RELATIONS: Relation[] = [ AField: `default_group`, BField: `default_group_for_meeting` }), + ...makeO2O({ + AViewModel: ViewMeeting, + BViewModel: ViewGroup, + AField: `anonymous_group`, + BField: `anonymous_group_for_meeting` + }), ...makeO2O({ AViewModel: ViewMeeting, BViewModel: ViewGroup, diff --git a/client/src/app/site/modules/global-headbar/components/account-button/account-button.component.html b/client/src/app/site/modules/global-headbar/components/account-button/account-button.component.html index 90bdca4abf..b0cf5113a9 100644 --- a/client/src/app/site/modules/global-headbar/components/account-button/account-button.component.html +++ b/client/src/app/site/modules/global-headbar/components/account-button/account-button.component.html @@ -1,7 +1,11 @@
- {{ username }} + @if (isLoggedIn) { + {{ username }} + } @else { + {{ 'Public Access' | translate }} + }
@@ -15,7 +19,13 @@
-
{{ username }}
+
+ @if (isLoggedIn) { + {{ username }} + } @else { + {{ 'Public Access' | translate }} + } +
@if (user) {
{{ getOmlVerboseName() | translate }}
} diff --git a/client/src/app/site/modules/global-headbar/components/account-button/account-button.component.ts b/client/src/app/site/modules/global-headbar/components/account-button/account-button.component.ts index 2054c2da0e..ccf97598e7 100644 --- a/client/src/app/site/modules/global-headbar/components/account-button/account-button.component.ts +++ b/client/src/app/site/modules/global-headbar/components/account-button/account-button.component.ts @@ -135,7 +135,11 @@ export class AccountButtonComponent extends BaseUiComponent implements OnInit { public async login(): Promise { await this.authService.logoutAnonymous(); - this.router.navigate([`/`, this.activeMeetingId, `login`]); + if (this.activeMeetingId) { + this.router.navigate([`/`, this.activeMeetingId, `login`]); + } else { + this.router.navigate([`/`, `login`]); + } } public async logout(): Promise { @@ -181,7 +185,7 @@ export class AccountButtonComponent extends BaseUiComponent implements OnInit { private onOperatorUpdate(): void { this.isLoggedIn = !this.operator.isAnonymous; - this.username = this.isLoggedIn ? this.operator.shortName : this.translate.instant(`Guest`); + this.username = this.operator.shortName; const userId = this.operator.operatorId; if (this._userId !== userId) { this._userId = userId; diff --git a/client/src/app/site/pages/login/pages/login-mask/components/login-mask/login-mask.component.html b/client/src/app/site/pages/login/pages/login-mask/components/login-mask/login-mask.component.html index dfac839708..2be7757bda 100644 --- a/client/src/app/site/pages/login/pages/login-mask/components/login-mask/login-mask.component.html +++ b/client/src/app/site/pages/login/pages/login-mask/components/login-mask/login-mask.component.html @@ -43,7 +43,7 @@


@if (guestsEnabled) { }
@@ -93,7 +93,6 @@


- @if (guestsEnabled && showExtra) { } diff --git a/client/src/app/site/pages/login/pages/login-mask/components/login-mask/login-mask.component.ts b/client/src/app/site/pages/login/pages/login-mask/components/login-mask/login-mask.component.ts index 3fbea6454d..50f5aac231 100644 --- a/client/src/app/site/pages/login/pages/login-mask/components/login-mask/login-mask.component.ts +++ b/client/src/app/site/pages/login/pages/login-mask/components/login-mask/login-mask.component.ts @@ -8,7 +8,7 @@ import { Meeting } from 'src/app/domain/models/meetings/meeting'; import { fadeInAnim } from 'src/app/infrastructure/animations'; import { BaseMeetingComponent } from 'src/app/site/pages/meetings/base/base-meeting.component'; import { ViewMeeting } from 'src/app/site/pages/meetings/view-models/view-meeting'; -import { OrganizationService } from 'src/app/site/pages/organization/services/organization.service'; +import { ORGANIZATION_ID, OrganizationService } from 'src/app/site/pages/organization/services/organization.service'; import { OrganizationSettingsService } from 'src/app/site/pages/organization/services/organization-settings.service'; import { ViewOrganization } from 'src/app/site/pages/organization/view-models/view-organization'; import { AuthService } from 'src/app/site/services/auth.service'; @@ -90,6 +90,7 @@ export class LoginMaskComponent extends BaseMeetingComponent implements OnInit, private loginMessage = `Loading data. Please wait ...`; private currentMeetingId: number | null = null; + private guestMeetingId: number | null = null; public constructor( protected override translate: TranslateService, @@ -134,6 +135,8 @@ export class LoginMaskComponent extends BaseMeetingComponent implements OnInit, this.route.params.subscribe(params => { if (params[`meetingId`]) { this.loadMeeting(params[`meetingId`]); + } else { + this.loadActiveMeetings(); } }); @@ -184,7 +187,7 @@ export class LoginMaskComponent extends BaseMeetingComponent implements OnInit, public async guestLogin(): Promise { await this.authService.anonLogin(); - this.osRouter.navigateAfterLogin(this.currentMeetingId); + this.osRouter.navigateAfterLogin(this.currentMeetingId || this.guestMeetingId); } public async samlLogin(): Promise { @@ -239,6 +242,26 @@ export class LoginMaskComponent extends BaseMeetingComponent implements OnInit, this.guestsEnabled = this.meeting.enable_anonymous; } + private async loadActiveMeetings(): Promise { + const resp = await this.autoupdate.single( + await this.modelRequestBuilder.build({ + ids: [ORGANIZATION_ID], + viewModelCtor: ViewOrganization, + follow: [{ idField: `active_meeting_ids`, fieldset: [`enable_anonymous`] }] + }), + `meeting_login` + ); + if (!resp || !resp[`meeting`]) { + return; + } + const publicMeetings = Object.values(resp[`meeting`]).filter(m => m[`enable_anonymous`]); + + this.guestsEnabled = !!publicMeetings.length; + if (publicMeetings.length === 1) { + this.guestMeetingId = publicMeetings[0][`id`]; + } + } + private checkDevice(): void { if (!this.browserSupport.isBrowserSupported()) { this.router.navigate([`./unsupported-browser`], { relativeTo: this.route }); diff --git a/client/src/app/site/pages/meetings/meetings-routing.module.ts b/client/src/app/site/pages/meetings/meetings-routing.module.ts index 69da5bf801..33e401572f 100644 --- a/client/src/app/site/pages/meetings/meetings-routing.module.ts +++ b/client/src/app/site/pages/meetings/meetings-routing.module.ts @@ -13,9 +13,7 @@ const routes: Routes = [ children: [ { path: ``, - loadChildren: () => import(`./pages/home/home.module`).then(m => m.HomeModule), - data: { meetingPermissions: [Permission.meetingCanSeeFrontpage] }, - canLoad: [PermissionGuard] + loadChildren: () => import(`./pages/home/home.module`).then(m => m.HomeModule) }, { path: `agenda`, diff --git a/client/src/app/site/pages/meetings/modules/meetings-navigation/components/meetings-navigation-wrapper/meetings-navigation-wrapper.component.ts b/client/src/app/site/pages/meetings/modules/meetings-navigation/components/meetings-navigation-wrapper/meetings-navigation-wrapper.component.ts index 560198c0eb..c69376e0ac 100644 --- a/client/src/app/site/pages/meetings/modules/meetings-navigation/components/meetings-navigation-wrapper/meetings-navigation-wrapper.component.ts +++ b/client/src/app/site/pages/meetings/modules/meetings-navigation/components/meetings-navigation-wrapper/meetings-navigation-wrapper.component.ts @@ -38,10 +38,7 @@ export class MeetingsNavigationWrapperComponent extends BaseMeetingComponent imp } public get showMeetingNav(): boolean { - return ( - !this.operator.isAnonymous && - (this.operator.knowsMultipleMeetings || this.operator.hasOrganizationPermissions()) - ); + return this.operator.knowsMultipleMeetings || this.operator.hasOrganizationPermissions(); } public get meeting(): ViewMeeting | null { diff --git a/client/src/app/site/pages/meetings/modules/poll/components/base-poll-form/base-poll-form.component.html b/client/src/app/site/pages/meetings/modules/poll/components/base-poll-form/base-poll-form.component.html index 3e59be74cc..aba0ec4fbd 100644 --- a/client/src/app/site/pages/meetings/modules/poll/components/base-poll-form/base-poll-form.component.html +++ b/client/src/app/site/pages/meetings/modules/poll/components/base-poll-form/base-poll-form.component.html @@ -36,7 +36,7 @@

groupA.weight - groupB.weight; diff --git a/client/src/app/site/pages/meetings/pages/home/home-routing.module.ts b/client/src/app/site/pages/meetings/pages/home/home-routing.module.ts index 7e7d5e7ecf..f8ba342025 100644 --- a/client/src/app/site/pages/meetings/pages/home/home-routing.module.ts +++ b/client/src/app/site/pages/meetings/pages/home/home-routing.module.ts @@ -1,11 +1,15 @@ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; +import { Permission } from 'src/app/domain/definitions/permission'; +import { PermissionGuard } from 'src/app/site/guards/permission.guard'; const routes: Routes = [ { path: ``, pathMatch: `full`, - loadChildren: () => import(`./pages/start/start.module`).then(m => m.StartModule) + loadChildren: () => import(`./pages/start/start.module`).then(m => m.StartModule), + data: { meetingPermissions: [Permission.meetingCanSeeFrontpage] }, + canLoad: [PermissionGuard] }, { path: `info`, diff --git a/client/src/app/site/pages/meetings/pages/meeting-settings/pages/meeting-settings-group-detail/components/meeting-settings-group-detail-field/meeting-settings-group-detail-field.component.ts b/client/src/app/site/pages/meetings/pages/meeting-settings/pages/meeting-settings-group-detail/components/meeting-settings-group-detail-field/meeting-settings-group-detail-field.component.ts index 480b7a216e..79b76f94be 100644 --- a/client/src/app/site/pages/meetings/pages/meeting-settings/pages/meeting-settings-group-detail/components/meeting-settings-group-detail-field/meeting-settings-group-detail-field.component.ts +++ b/client/src/app/site/pages/meetings/pages/meeting-settings/pages/meeting-settings-group-detail/components/meeting-settings-group-detail-field/meeting-settings-group-detail-field.component.ts @@ -194,7 +194,7 @@ export class MeetingSettingsGroupDetailFieldComponent extends BaseComponent impl public ngOnInit(): void { // filter out empty results in group observable. We never have no groups and it messes up // the settings change detection - this.groupObservable = this.groupRepo.getViewModelListWithoutDefaultGroupObservable().pipe( + this.groupObservable = this.groupRepo.getViewModelListWithoutSystemGroupsObservable().pipe( filter(groups => !!groups.length), map(groups => this.getRestrictedValue(groups)) ); diff --git a/client/src/app/site/pages/meetings/pages/motions/pages/comments/components/comment-section-list/comment-section-list.component.ts b/client/src/app/site/pages/meetings/pages/motions/pages/comments/components/comment-section-list/comment-section-list.component.ts index 0926d2b9bd..44f32037ed 100644 --- a/client/src/app/site/pages/meetings/pages/motions/pages/comments/components/comment-section-list/comment-section-list.component.ts +++ b/client/src/app/site/pages/meetings/pages/motions/pages/comments/components/comment-section-list/comment-section-list.component.ts @@ -67,7 +67,7 @@ export class CommentSectionListComponent extends BaseComponent implements OnInit public ngOnInit(): void { super.setTitle(`Comment fields`); - this.groups = this.groupRepo.getViewModelListObservable(); + this.groups = this.groupRepo.getViewModelListWithoutAnonymousGroupObservable(); this.subscriptions.push( this.repo .getViewModelListObservable() diff --git a/client/src/app/site/pages/meetings/pages/motions/services/common/motion-permission.service/motion-permission.service.ts b/client/src/app/site/pages/meetings/pages/motions/services/common/motion-permission.service/motion-permission.service.ts index 32d0bf57fc..0e876d5917 100644 --- a/client/src/app/site/pages/meetings/pages/motions/services/common/motion-permission.service/motion-permission.service.ts +++ b/client/src/app/site/pages/meetings/pages/motions/services/common/motion-permission.service/motion-permission.service.ts @@ -57,6 +57,7 @@ export class MotionPermissionService { public canDoActionWhileDelegationEnabled(isAdditionalDelegationSettingEnabled: boolean): boolean { return !( + !this.operator.isAnonymous && this.operator.user.isVoteRightDelegated && this._delegationEnabled && isAdditionalDelegationSettingEnabled diff --git a/client/src/app/site/pages/meetings/pages/participants/modules/groups/components/group-list/group-list.component.html b/client/src/app/site/pages/meetings/pages/participants/modules/groups/components/group-list/group-list.component.html index cebd6d404a..9b5ee6d0a2 100644 --- a/client/src/app/site/pages/meetings/pages/participants/modules/groups/components/group-list/group-list.component.html +++ b/client/src/app/site/pages/meetings/pages/participants/modules/groups/components/group-list/group-list.component.html @@ -57,16 +57,20 @@

{{ 'Groups' | translate }}

- @if (!group.isAdminGroup) { + @if ( + group.isAdminGroup || (group.isAnonymousGroup && !perm.anon_allowed) + ) { + + } @else { } - @if (group.isAdminGroup) { - - }
diff --git a/client/src/app/site/pages/meetings/pages/participants/modules/groups/components/group-list/group-list.component.ts b/client/src/app/site/pages/meetings/pages/participants/modules/groups/components/group-list/group-list.component.ts index be2822c3d6..a3dc30e7a1 100644 --- a/client/src/app/site/pages/meetings/pages/participants/modules/groups/components/group-list/group-list.component.ts +++ b/client/src/app/site/pages/meetings/pages/participants/modules/groups/components/group-list/group-list.component.ts @@ -97,7 +97,12 @@ export class GroupListComponent extends BaseMeetingComponent implements OnInit, this.repo.getViewModelListObservable().subscribe(newViewGroups => { if (newViewGroups) { - this.groups = newViewGroups.slice().sort((groupA, groupB) => groupA.weight - groupB.weight); + if (!this.meetingSettingsService.instant(`enable_anonymous`)) { + newViewGroups = newViewGroups.filter(group => !group.anonymous_group_for_meeting_id); + } else { + newViewGroups = newViewGroups.slice(); + } + this.groups = newViewGroups.sort((groupA, groupB) => groupA.weight - groupB.weight); this.updateRowDef(); } }); @@ -246,7 +251,7 @@ export class GroupListComponent extends BaseMeetingComponent implements OnInit, * @param group ViewGroup */ public isProtected(group: ViewGroup): boolean { - return group.isAdminGroup || group.isDefaultGroup; + return group.isAdminGroup || group.isDefaultGroup || group.isAnonymousGroup; } /** diff --git a/client/src/app/site/pages/meetings/pages/participants/modules/groups/services/group-controller.service.ts b/client/src/app/site/pages/meetings/pages/participants/modules/groups/services/group-controller.service.ts index fcffcccbc0..6351f7e50f 100644 --- a/client/src/app/site/pages/meetings/pages/participants/modules/groups/services/group-controller.service.ts +++ b/client/src/app/site/pages/meetings/pages/participants/modules/groups/services/group-controller.service.ts @@ -62,11 +62,26 @@ export class GroupControllerService extends BaseMeetingControllerService groups.filter(group => !group.isDefaultGroup)); } + public getFilterSystemGroupFn(): OperatorFunction { + return map(groups => groups.filter(group => !group.isDefaultGroup && !group.isAnonymousGroup)); + } + + public getFilterAnonymousGroupFn(): OperatorFunction { + return map(groups => groups.filter(group => !group.isAnonymousGroup)); + } + + /** + * Returns an Observable for all groups except the default and anonymous group. + */ + public getViewModelListWithoutSystemGroupsObservable(): Observable { + return this.getViewModelListObservable().pipe(this.getFilterSystemGroupFn()); + } + /** - * Returns an Observable for all groups except the default group. + * Returns an Observable for all groups except the anonymous group. */ - public getViewModelListWithoutDefaultGroupObservable(): Observable { - return this.getViewModelListObservable().pipe(this.getFilterDefaultGroupFn()); + public getViewModelListWithoutAnonymousGroupObservable(): Observable { + return this.getViewModelListObservable().pipe(this.getFilterAnonymousGroupFn()); } public getNameForIds(...ids: number[]): string { diff --git a/client/src/app/site/pages/meetings/pages/participants/modules/groups/view-models/view-group.ts b/client/src/app/site/pages/meetings/pages/participants/modules/groups/view-models/view-group.ts index bd21a3d0ca..a9d22edd25 100644 --- a/client/src/app/site/pages/meetings/pages/participants/modules/groups/view-models/view-group.ts +++ b/client/src/app/site/pages/meetings/pages/participants/modules/groups/view-models/view-group.ts @@ -27,6 +27,7 @@ export class ViewGroup extends BaseHasMeetingUsersViewModel { interface IGroupRelations { default_group_for_meeting: ViewMeeting; admin_group_for_meeting: ViewMeeting; + anonymous_group_for_meeting: ViewMeeting; meeting_mediafile_access_groups: ViewMediafile[]; meeting_mediafile_inherited_access_groups: ViewMediafile[]; read_comment_sections: ViewMotionCommentSection[]; diff --git a/client/src/app/site/pages/meetings/pages/participants/pages/participant-detail/components/participant-detail-view/participant-detail-view.component.ts b/client/src/app/site/pages/meetings/pages/participants/pages/participant-detail/components/participant-detail-view/participant-detail-view.component.ts index a961f6f871..efd35f2b9c 100644 --- a/client/src/app/site/pages/meetings/pages/participants/pages/participant-detail/components/participant-detail-view/participant-detail-view.component.ts +++ b/client/src/app/site/pages/meetings/pages/participants/pages/participant-detail/components/participant-detail-view/participant-detail-view.component.ts @@ -198,7 +198,7 @@ export class ParticipantDetailViewComponent extends BaseMeetingComponent { this.structureLevelObservable = this.structureLevelRepo.getViewModelListObservable(); // TODO: Open groups subscription - this.groups = this.groupRepo.getViewModelListWithoutDefaultGroupObservable(); + this.groups = this.groupRepo.getViewModelListWithoutSystemGroupsObservable(); } public isAllowed(action: string): boolean { diff --git a/client/src/app/site/pages/meetings/pages/participants/pages/participant-detail/pages/participant-detail-manage/components/participant-create-wizard/participant-create-wizard.component.ts b/client/src/app/site/pages/meetings/pages/participants/pages/participant-detail/pages/participant-detail-manage/components/participant-create-wizard/participant-create-wizard.component.ts index af42444849..a85aa4d88d 100644 --- a/client/src/app/site/pages/meetings/pages/participants/pages/participant-detail/pages/participant-detail-manage/components/participant-create-wizard/participant-create-wizard.component.ts +++ b/client/src/app/site/pages/meetings/pages/participants/pages/participant-detail/pages/participant-detail-manage/components/participant-create-wizard/participant-create-wizard.component.ts @@ -192,7 +192,7 @@ export class ParticipantCreateWizardComponent extends BaseMeetingComponent imple public ngOnInit(): void { // TODO: Fetch groups for repo search selection - this.groupsObservable = this.groupRepo.getViewModelListWithoutDefaultGroupObservable(); + this.groupsObservable = this.groupRepo.getViewModelListWithoutSystemGroupsObservable(); this.structureLevelObservable = this.structureLevelRepo.getViewModelListObservable(); diff --git a/client/src/app/site/pages/meetings/pages/participants/pages/participant-list/components/participant-list/participant-list.component.ts b/client/src/app/site/pages/meetings/pages/participants/pages/participant-list/components/participant-list/participant-list.component.ts index 1ce9a0679e..68006acfd9 100644 --- a/client/src/app/site/pages/meetings/pages/participants/pages/participant-list/components/participant-list/participant-list.component.ts +++ b/client/src/app/site/pages/meetings/pages/participants/pages/participant-list/components/participant-list/participant-list.component.ts @@ -53,7 +53,7 @@ export class ParticipantListComponent extends BaseMeetingListViewComponent = this.groupRepo.getViewModelListWithoutDefaultGroupObservable(); + public groupsObservable: Observable = this.groupRepo.getViewModelListWithoutSystemGroupsObservable(); /** * All available structure level, where the user can be in. diff --git a/client/src/app/site/pages/meetings/pages/participants/pages/participant-list/modules/participant-list-info-dialog/components/participant-list-info-dialog/participant-list-info-dialog.component.ts b/client/src/app/site/pages/meetings/pages/participants/pages/participant-list/modules/participant-list-info-dialog/components/participant-list-info-dialog/participant-list-info-dialog.component.ts index 098e5022a9..69583e3a02 100644 --- a/client/src/app/site/pages/meetings/pages/participants/pages/participant-list/modules/participant-list-info-dialog/components/participant-list-info-dialog/participant-list-info-dialog.component.ts +++ b/client/src/app/site/pages/meetings/pages/participants/pages/participant-list/modules/participant-list-info-dialog/components/participant-list-info-dialog/participant-list-info-dialog.component.ts @@ -24,7 +24,7 @@ export class ParticipantListInfoDialogComponent extends BaseUiComponent implemen public readonly genders = GENDERS; public get groupsObservable(): Observable { - return this.groupRepo.getViewModelListWithoutDefaultGroupObservable(); + return this.groupRepo.getViewModelListWithoutSystemGroupsObservable(); } public get otherParticipantsObservable(): Observable { diff --git a/client/src/app/site/pages/meetings/services/active-meeting.subscription.ts b/client/src/app/site/pages/meetings/services/active-meeting.subscription.ts index 50e8aee979..53784045d6 100644 --- a/client/src/app/site/pages/meetings/services/active-meeting.subscription.ts +++ b/client/src/app/site/pages/meetings/services/active-meeting.subscription.ts @@ -70,6 +70,7 @@ export function getActiveMeetingSubscriptionConfig(id: Id, settingsKeys: string[ idField: `group_ids`, fieldset: [ `admin_group_for_meeting_id`, + `anonymous_group_for_meeting_id`, `default_group_for_meeting_id`, `name`, `permissions`, @@ -122,6 +123,7 @@ export function getActiveMeetingSubscriptionConfig(id: Id, settingsKeys: string[ `jitsi_room_name`, `jitsi_room_password`, `admin_group_id`, + `anonymous_group_id`, `default_group_id` ] }, diff --git a/client/src/app/site/pages/meetings/services/meeting-settings-definition.service/meeting-settings-definitions.ts b/client/src/app/site/pages/meetings/services/meeting-settings-definition.service/meeting-settings-definitions.ts index 17be8f7d99..bc25bdd50d 100644 --- a/client/src/app/site/pages/meetings/services/meeting-settings-definition.service/meeting-settings-definitions.ts +++ b/client/src/app/site/pages/meetings/services/meeting-settings-definition.service/meeting-settings-definitions.ts @@ -152,16 +152,6 @@ export const meetingSettings: SettingsGroup[] = fillInSettingsDefaults([ label: _(`General`), icon: `home`, subgroups: [ - { - label: _(`System`), - settings: [ - { - key: `enable_anonymous`, - label: _(`Allow access for anonymous guest users`), - type: `boolean` - } - ] - }, { label: _(`Meeting information`), settings: [ @@ -195,7 +185,12 @@ export const meetingSettings: SettingsGroup[] = fillInSettingsDefaults([ { key: `external_id`, label: _(`External ID`) - }, + } + ] + }, + { + label: _(`System`), + settings: [ { key: `locked_from_inside`, label: _(`Activate closed meeting`), @@ -204,6 +199,11 @@ export const meetingSettings: SettingsGroup[] = fillInSettingsDefaults([ ), type: `boolean`, forbidden: meetingView => meetingView.isTemplate + }, + { + key: `enable_anonymous`, + label: _(`Allow access for anonymous guest users`), + type: `boolean` } ] }, diff --git a/client/src/app/site/pages/meetings/view-models/view-meeting.ts b/client/src/app/site/pages/meetings/view-models/view-meeting.ts index 89dbb7dfa9..0220338204 100644 --- a/client/src/app/site/pages/meetings/view-models/view-meeting.ts +++ b/client/src/app/site/pages/meetings/view-models/view-meeting.ts @@ -186,6 +186,7 @@ interface IMeetingRelations { reference_projector: ViewProjector; projections: ViewProjection[]; default_group: ViewGroup; + anonymous_group: ViewGroup; admin_group: ViewGroup; is_active_in_organization: ViewOrganization; is_archived_in_organization: ViewOrganization; diff --git a/client/src/app/site/pages/organization/modules/navigation/organization-navigation/organization-navigation.component.ts b/client/src/app/site/pages/organization/modules/navigation/organization-navigation/organization-navigation.component.ts index 678e8c2fb4..230d352d0c 100644 --- a/client/src/app/site/pages/organization/modules/navigation/organization-navigation/organization-navigation.component.ts +++ b/client/src/app/site/pages/organization/modules/navigation/organization-navigation/organization-navigation.component.ts @@ -3,6 +3,7 @@ import { CML, OML } from 'src/app/domain/definitions/organization-permission'; import { BaseMenuEntry, getCustomStyleForEntry } from 'src/app/site/base/base-menu-entry'; import { MainMenuService } from 'src/app/site/pages/meetings/services/main-menu.service'; import { AuthService } from 'src/app/site/services/auth.service'; +import { OperatorService } from 'src/app/site/services/operator.service'; import { ViewPortService } from 'src/app/site/services/view-port.service'; interface OrgaMenuEntry extends BaseMenuEntry { @@ -78,9 +79,14 @@ export class OrganizationNavigationComponent { public constructor( private authService: AuthService, + private operator: OperatorService, private menuService: MainMenuService, private vp: ViewPortService - ) {} + ) { + if (this.operator.isAnonymous) { + this.menuEntries = [this.menuEntries[0]]; + } + } public getCustomStyleForEntry(entry: OrgaMenuEntry): { [key: string]: any } { return getCustomStyleForEntry(entry); diff --git a/client/src/app/site/pages/organization/pages/committees/modules/committee-meeting-preview/committee-meeting-preview.component.html b/client/src/app/site/pages/organization/pages/committees/modules/committee-meeting-preview/committee-meeting-preview.component.html index 2cc1e68136..227326b7c8 100644 --- a/client/src/app/site/pages/organization/pages/committees/modules/committee-meeting-preview/committee-meeting-preview.component.html +++ b/client/src/app/site/pages/organization/pages/committees/modules/committee-meeting-preview/committee-meeting-preview.component.html @@ -4,6 +4,11 @@ [ngClass]="{ 'background-accent': meeting.isActive, 'background-dark-brighter': meeting.isArchived }" > + @if (meeting.enable_anonymous) { +
+ public +
+ } @if (meeting.locked_from_inside) {
lock diff --git a/client/src/app/site/pages/organization/pages/dashboard/dashboard.subscription.ts b/client/src/app/site/pages/organization/pages/dashboard/dashboard.subscription.ts index 0ac56ed7bd..989760be2f 100644 --- a/client/src/app/site/pages/organization/pages/dashboard/dashboard.subscription.ts +++ b/client/src/app/site/pages/organization/pages/dashboard/dashboard.subscription.ts @@ -13,6 +13,7 @@ export const meetingFields: (keyof Meeting)[] = [ `committee_id`, `is_active_in_organization_id`, `template_for_organization_id`, + `enable_anonymous`, `description`, `location`, `organization_tag_ids`, diff --git a/client/src/app/site/pages/organization/pages/dashboard/pages/dashboard-detail/components/dashboard/dashboard.component.html b/client/src/app/site/pages/organization/pages/dashboard/pages/dashboard-detail/components/dashboard/dashboard.component.html index f9d9f1df05..1a6e6321d8 100644 --- a/client/src/app/site/pages/organization/pages/dashboard/pages/dashboard-detail/components/dashboard/dashboard.component.html +++ b/client/src/app/site/pages/organization/pages/dashboard/pages/dashboard-detail/components/dashboard/dashboard.component.html @@ -10,109 +10,111 @@

{{ 'Calendar' | translate }}

} -@if (noMeetingsToShow && ready) { - - - {{ 'No meetings available' | translate }} - - -} -@if (noMeetingsToShow && !ready) { -
- -
-} -@if (!noMeetingsToShow) { -
- - @if (organizationDescription) { -
-
-
- } -
-
- access_alarm - {{ 'today' | translate }} -
-
- @if (!currentMeetings.length) { - - - {{ 'No meetings available' | translate }} - - - } - @for (meeting of currentMeetings; track meeting; let last = $last) { - - @if (!last) { - - } - } -
+@if (noMeetingsToShow) { + @if (ready) { + + + {{ 'No meetings available' | translate }} + + + } @else { +
+
- -
-
- update - {{ 'future' | translate }} -
- + + @if (organizationDescription) { +
+
+
+ } +
+
+ access_alarm + {{ 'today' | translate }} +
+
+ @if (!currentMeetings.length) { + + + {{ 'No meetings available' | translate }} + + } - }" - > -
- -
-
- - {{ 'ended' | translate }} -
- + @if (!last) { + + } } - }" - > -
- - @if (!noNoDateMeetingsToShow) { -
+
+
+ +
- - {{ 'dateless' | translate }} + update + {{ 'future' | translate }}
- } -
+ +
+
+ + {{ 'ended' | translate }} +
+ +
+ + @if (!noNoDateMeetingsToShow) { +
+
+ + {{ 'dateless' | translate }} +
+ +
+ } +
+ } } @@ -158,73 +160,63 @@

{{ 'Calendar' | translate }}

[ngClass]="context?.cssClass" [routerLink]="meeting.id" > -
-
- -
{{ meeting.name }}
-
-
-
- @if (meeting.location) { - {{ meeting.location }} - } - @if (meeting.location && (meeting.start_time || meeting.end_time)) { -  ·  - } - @if (meeting.start_time || meeting.end_time) { - - - - } -
-
+ } @else {
-
-
- -
{{ meeting.name }}
-
-
-
- @if (meeting.location) { - {{ meeting.location }} - } - @if (meeting.location && (meeting.start_time || meeting.end_time)) { -  ·  - } - @if (meeting.start_time || meeting.end_time) { - - - - } -
-
+
} @if (meeting.isArchived) { archive } - public + } + @if (meeting.committee_id) { + + layers + + } +
+ + + +
+
+ +
{{ meeting.name }}
+
+
+
- layers - + @if (meeting.location) { + {{ meeting.location }} + } + @if (meeting.location && (meeting.start_time || meeting.end_time)) { +  ·  + } + @if (meeting.start_time || meeting.end_time) { + + + + } +
diff --git a/client/src/app/site/pages/organization/pages/dashboard/pages/dashboard-detail/components/dashboard/dashboard.component.scss b/client/src/app/site/pages/organization/pages/dashboard/pages/dashboard-detail/components/dashboard/dashboard.component.scss index 865250a2b5..ae31abc130 100644 --- a/client/src/app/site/pages/organization/pages/dashboard/pages/dashboard-detail/components/dashboard/dashboard.component.scss +++ b/client/src/app/site/pages/organization/pages/dashboard/pages/dashboard-detail/components/dashboard/dashboard.component.scss @@ -253,6 +253,7 @@ os-dashboard { .meeting-box-left { width: calc(95% - 50px); + flex-grow: 1; @media (max-width: 960px) { width: 90%; diff --git a/client/src/app/site/pages/organization/pages/dashboard/pages/dashboard-detail/components/dashboard/dashboard.component.ts b/client/src/app/site/pages/organization/pages/dashboard/pages/dashboard-detail/components/dashboard/dashboard.component.ts index dbb7c16989..072f3101b9 100644 --- a/client/src/app/site/pages/organization/pages/dashboard/pages/dashboard-detail/components/dashboard/dashboard.component.ts +++ b/client/src/app/site/pages/organization/pages/dashboard/pages/dashboard-detail/components/dashboard/dashboard.component.ts @@ -83,7 +83,10 @@ export class DashboardComponent extends BaseComponent { this.subscriptions.push( this.meetingRepo.getViewModelListObservable().subscribe(meetings => { const filteredMeetings = meetings.filter( - meeting => this.operator.isInMeeting(meeting.id) || this.operator.isSuperAdmin + meeting => + this.operator.isInMeeting(meeting.id) || + this.operator.isSuperAdmin || + (meeting.enable_anonymous && this.operator.isAnonymous) ); const currentDate = new Date(); currentDate.setHours(0, 0, 0, 0); diff --git a/client/src/app/site/pages/organization/pages/orga-meetings/pages/meeting-list/components/meeting-list/meeting-list.component.html b/client/src/app/site/pages/organization/pages/orga-meetings/pages/meeting-list/components/meeting-list/meeting-list.component.html index 36e2f5a324..8bb89ad4f5 100644 --- a/client/src/app/site/pages/organization/pages/orga-meetings/pages/meeting-list/components/meeting-list/meeting-list.component.html +++ b/client/src/app/site/pages/organization/pages/orga-meetings/pages/meeting-list/components/meeting-list/meeting-list.component.html @@ -54,7 +54,14 @@

{{ 'Meetings' | translate }}

} - + + @if (meeting.enable_anonymous) { + + public + + } + + @if (meeting.locked_from_inside) { lock diff --git a/client/src/app/site/pages/organization/pages/orga-meetings/pages/meeting-list/services/meeting-list-filter/meeting-list-filter.service.ts b/client/src/app/site/pages/organization/pages/orga-meetings/pages/meeting-list/services/meeting-list-filter/meeting-list-filter.service.ts index 332fa8ff9e..d3a89dffa9 100644 --- a/client/src/app/site/pages/organization/pages/orga-meetings/pages/meeting-list/services/meeting-list-filter/meeting-list-filter.service.ts +++ b/client/src/app/site/pages/organization/pages/orga-meetings/pages/meeting-list/services/meeting-list-filter/meeting-list-filter.service.ts @@ -45,6 +45,14 @@ export class MeetingListFilterService extends BaseFilterListService { label: _(`Is not archived`), condition: [false, null] } ] }, + { + property: `enable_anonymous`, + label: _(`Public`), + options: [ + { label: _(`Is public`), condition: true }, + { label: _(`Is not public`), condition: [false, null] } + ] + }, { property: `relatedTime`, label: _(`Time`), diff --git a/client/src/app/site/services/auth-check.service.ts b/client/src/app/site/services/auth-check.service.ts index a91b85ecb1..36c42aab37 100644 --- a/client/src/app/site/services/auth-check.service.ts +++ b/client/src/app/site/services/auth-check.service.ts @@ -1,5 +1,6 @@ import { Injectable } from '@angular/core'; import { Data } from '@angular/router'; +import { CookieService } from 'ngx-cookie-service'; import { Id } from 'src/app/domain/definitions/key-types'; import { CML, OML } from 'src/app/domain/definitions/organization-permission'; import { Permission } from 'src/app/domain/definitions/permission'; @@ -39,6 +40,7 @@ export class AuthCheckService { private meetingRepo: MeetingRepositoryService, private meetingSettingsService: MeetingSettingsService, private autoupdate: AutoupdateService, + private cookie: CookieService, private modelRequestBuilder: ModelRequestBuilderService, private osRouter: OpenSlidesRouterService ) {} @@ -69,7 +71,11 @@ export class AuthCheckService { meeting = this.meetingRepo.getViewModel(+meetingIdString); } - return (this.operator.isAnonymous && meeting?.enable_anonymous) || this.operator.isAuthenticated; + return ( + (!meeting && this.cookie.check(`anonymous-auth`)) || + (this.operator.isAnonymous && meeting?.enable_anonymous) || + this.operator.isAuthenticated + ); } public async isAuthorizedToSeeOrganization(): Promise { diff --git a/client/src/app/site/services/auth.service.ts b/client/src/app/site/services/auth.service.ts index 5ec4cca6ba..3ab464dc58 100644 --- a/client/src/app/site/services/auth.service.ts +++ b/client/src/app/site/services/auth.service.ts @@ -96,7 +96,8 @@ export class AuthService { public async anonLogin(): Promise { this.cookie.set(`anonymous-auth`, ``, { - sameSite: `Strict` + sameSite: `Strict`, + path: `/` }); return; } @@ -150,7 +151,7 @@ export class AuthService { } public async logoutAnonymous(): Promise { - this.cookie.delete(`anonymous-auth`); + this.cookie.delete(`anonymous-auth`, `/`); } public isAuthenticated(): boolean { diff --git a/client/src/app/site/services/operator.service.ts b/client/src/app/site/services/operator.service.ts index 6141640746..29c59d6702 100644 --- a/client/src/app/site/services/operator.service.ts +++ b/client/src/app/site/services/operator.service.ts @@ -30,6 +30,7 @@ import { NoActiveMeetingError } from '../pages/meetings/services/active-meeting- import { MeetingControllerService } from '../pages/meetings/services/meeting-controller.service'; import { MeetingSettingsService } from '../pages/meetings/services/meeting-settings.service'; import { ViewMeeting } from '../pages/meetings/view-models/view-meeting'; +import { OrganizationService } from '../pages/organization/services/organization.service'; import { AuthService } from './auth.service'; import { AutoupdateService, ModelSubscription } from './autoupdate'; import { DataStoreService } from './data-store.service'; @@ -103,7 +104,10 @@ export class OperatorService { } public get knowsMultipleMeetings(): boolean { - return this.isAnyManager || this.user.hasMultipleMeetings; + return ( + this.isAnyManager || + (this.isAnonymous ? this.defaultAnonUser.hasMultipleMeetings : this.user.hasMultipleMeetings) + ); } public get onlyMeeting(): Id { @@ -196,6 +200,11 @@ export class OperatorService { return activeMeeting ? activeMeeting.default_group_id : null; } + private get anonymousGroupId(): number | null { + const activeMeeting = this.activeMeetingService.meeting; + return activeMeeting ? activeMeeting.anonymous_group_id : null; + } + private get adminGroupId(): number | null { const activeMeeting = this.activeMeetingService.meeting; return activeMeeting ? activeMeeting.admin_group_id : null; @@ -259,7 +268,8 @@ export class OperatorService { private autoupdateService: AutoupdateService, private modelRequestBuilder: ModelRequestBuilderService, private meetingRepo: MeetingControllerService, - private meetingSettings: MeetingSettingsService + private meetingSettings: MeetingSettingsService, + private organizationService: OrganizationService ) { this.setNotReady(); // General environment in which the operator moves @@ -333,7 +343,7 @@ export class OperatorService { if (!this.activeMeetingId || !group) { return; } - if (this.isAnonymous && group.id === this.defaultGroupId) { + if (this.isAnonymous && group.id === this.anonymousGroupId) { this._groupIds = this._groupIds || []; this._permissions = this.calcPermissions(); this._operatorUpdatedSubject.next(); @@ -347,6 +357,19 @@ export class OperatorService { } } }); + this.organizationService.organizationObservable.pipe().subscribe(organization => { + if (this.isAnonymous) { + this.defaultAnonUser = new ViewUser( + new User({ + id: 0, + first_name: `Guest`, + meeting_ids: + organization?.active_meetings.filter(m => m.enable_anonymous).map(m => m.id) || null + }) + ); + this._operatorUpdatedSubject.next(); + } + }); this.operatorUpdated.subscribe(() => { this.checkReadyState(); }); @@ -379,7 +402,7 @@ export class OperatorService { public isInMeeting(meetingId: Id): boolean { const meeting = this.meetingRepo.getViewModel(meetingId); - return (meeting.enable_anonymous && this.isAnonymous) || this.user.meeting_ids?.includes(meetingId) || false; + return (meeting?.enable_anonymous && this.isAnonymous) || this.user.meeting_ids?.includes(meetingId) || false; } private updateUser(user: ViewUser): void { @@ -521,8 +544,7 @@ export class OperatorService { private calcPermissions(): Permission[] { const permissionSet = new Set(); if (this.isAnonymous) { - // Anonymous is always in the default group. - this.activeMeeting?.default_group?.permissions.forEach(perm => permissionSet.add(perm)); + this.activeMeeting?.anonymous_group?.permissions?.forEach(perm => permissionSet.add(perm)); } else { if (this._groupIds?.length) { this.DS.getMany(Group, this._groupIds).forEach(group => { @@ -742,7 +764,7 @@ export class OperatorService { return false; } if (this.isAnonymous) { - return !!groupIds.find(id => id === this.defaultGroupId); // any anonymous is in the default group. + return !!groupIds.find(id => id === this.anonymousGroupId); } return groupIds.some(id => this._groupIds?.includes(id)); }