Skip to content

Commit

Permalink
feat: initial notifications component u#2899
Browse files Browse the repository at this point in the history
  • Loading branch information
juanfran committed Oct 24, 2023
1 parent 5198f51 commit 8de2a94
Show file tree
Hide file tree
Showing 20 changed files with 656 additions and 4 deletions.
14 changes: 13 additions & 1 deletion javascript/apps/taiga/src/app/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { ActivatedRoute, NavigationEnd, Router } from '@angular/router';
import { TranslocoService } from '@ngneat/transloco';
import { concatLatestFrom } from '@ngrx/effects';
import { Store } from '@ngrx/store';
import { User } from '@taiga/data';
import { NotificationType, User } from '@taiga/data';
import { Observable, of } from 'rxjs';
import { filter, map, share, switchMap } from 'rxjs/operators';
import { WsService } from '~/app/services/ws';
Expand All @@ -24,6 +24,7 @@ import { InputModalityDetector } from '@angular/cdk/a11y';
import { LanguageService } from './services/language/language.service';
import { ConfigService } from '@taiga/cdk/services/config';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { UserEventsActions } from './modules/auth/data-access/+state/actions/user.actions';

@Component({
selector: 'tg-root',
Expand Down Expand Up @@ -82,6 +83,17 @@ export class AppComponent {
void this.router.navigate(['/logout']);
});

this.wsService
.userEvents<{ notification: NotificationType }>('notifications.create')
.pipe(takeUntilDestroyed())
.subscribe((msg) => {
this.store.dispatch(
UserEventsActions.newNotification({
notification: msg.event.content.notification,
})
);
});

this.setBannerHeight();
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/**
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*
* Copyright (c) 2023-present Kaleidos INC
*/

import { createActionGroup, emptyProps, props } from '@ngrx/store';
import { NotificationType } from '@taiga/data';

export const UserActions = createActionGroup({
source: 'User',
events: {
'Set notification number': props<{
notifications: {
read: number;
total: number;
unread: number;
};
}>(),
'Init notification section': emptyProps(),
'Fetch notifications success': props<{
notifications: NotificationType[];
}>(),
},
});

export const UserEventsActions = createActionGroup({
source: 'User ws',
events: {
'new notification': props<{ notification: NotificationType }>(),
},
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/**
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*
* Copyright (c) 2023-present Kaleidos INC
*/

import { Injectable, inject } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import * as AuthActions from '../actions/auth.actions';
import { Store } from '@ngrx/store';
import { UsersApiService } from '@taiga/api';
import { exhaustMap, map } from 'rxjs';
import { UserActions, UserEventsActions } from '../actions/user.actions';

@Injectable({ providedIn: 'root' })
export class NotificationsEffects {
private actions$ = inject(Actions);
private store = inject(Store);
private usersApiService = inject(UsersApiService);

public notificationCount$ = createEffect(() => {
return this.actions$.pipe(
ofType(
AuthActions.setUser,
AuthActions.loginSuccess,
UserEventsActions.newNotification
),
exhaustMap(() => {
return this.usersApiService.notificationsCount().pipe(
map((notifications) => {
return UserActions.setNotificationNumber({ notifications });
})
);
})
);
});

public fetchNotifications$ = createEffect(() => {
return this.actions$.pipe(
ofType(UserActions.initNotificationSection),
exhaustMap(() => {
return this.usersApiService.notifications().pipe(
map((notifications) => {
return UserActions.fetchNotificationsSuccess({ notifications });
})
);
})
);
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,32 @@
* Copyright (c) 2023-present Kaleidos INC
*/

import { createFeature, on } from '@ngrx/store';
import { createFeature, createSelector, on } from '@ngrx/store';
import { User } from '@taiga/data';
import { userSettingsActions } from '~/app/modules/feature-user-settings/data-access/+state/actions/user-settings.actions';
import { createImmerReducer } from '~/app/shared/utils/store';
import * as AuthActions from '../actions/auth.actions';
import { UserActions } from '../actions/user.actions';
import { NotificationType } from '@taiga/data';

export interface AuthState {
user: User | null;
loginError: boolean;
showResetPasswordConfirmation: boolean;
notificationCount: {
read: number;
total: number;
unread: number;
} | null;
notifications: NotificationType[];
}

export const initialState: AuthState = {
user: null,
loginError: false,
showResetPasswordConfirmation: false,
notificationCount: null,
notifications: [],
};

export const reducer = createImmerReducer(
Expand Down Expand Up @@ -72,11 +82,29 @@ export const reducer = createImmerReducer(
state.user.lang = lang.code;
}

return state;
}),
on(UserActions.setNotificationNumber, (state, { notifications }) => {
state.notificationCount = notifications;

return state;
}),
on(UserActions.fetchNotificationsSuccess, (state, { notifications }) => {
state.notifications = notifications;

return state;
})
);

export const authFeature = createFeature({
name: 'auth',
reducer: reducer,
extraSelectors: ({ selectNotificationCount }) => ({
unreadNotificationsCount: createSelector(
selectNotificationCount,
(count) => {
return count?.unread ?? 0;
}
),
}),
});
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,14 @@ import { EffectsModule } from '@ngrx/effects';
import { AuthEffects } from './+state/effects/auth.effects';
import { authFeature } from './+state/reducers/auth.reducer';
import { RouterModule } from '@angular/router';
import { NotificationsEffects } from './+state/effects/notifications.effects';

@NgModule({
declarations: [],
imports: [
RouterModule,
StoreModule.forFeature(authFeature),
EffectsModule.forFeature([AuthEffects]),
EffectsModule.forFeature([AuthEffects, NotificationsEffects]),
],
})
export class DataAccessAuthModule {}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
Copyright (c) 2023-present Kaleidos INC
*/
@import url("tools/typography.css");
@import url("taiga-ui/mixins/wrapper.css");

:host {
align-items: center;
Expand Down Expand Up @@ -98,3 +99,33 @@ Copyright (c) 2023-present Kaleidos INC
--color-border: var(--color-secondary);
}
}

.header-actions {
display: flex;
gap: var(--spacing-16);
padding-inline-end: var(--spacing-16);
}

.count-notifications-wrapper {
position: relative;
}

.count-notifications {
align-items: center;
aspect-ratio: 1 / 1;
background: var(--color-red);
block-size: 16px;
border-radius: 50%;
color: var(--color-white);
display: flex;
font-size: 0.688rem;
font-weight: var(--font-weight-medium);
inset-inline-end: 0;
justify-content: center;
position: absolute;
z-index: var(--first-layer);
}

.count-overflow {
font-size: 0.5rem;
}
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,33 @@
</tui-hosted-dropdown>
</div>
<div class="right">
<div class="header-actions">
<tui-hosted-dropdown
[content]="notificationsDropdown"
[(open)]="openNotificationsDropdown">
<div class="count-notifications-wrapper">
<span
*ngIf="notificationsCount() > 0"
class="count-notifications"
[class.count-overflow]="notificationsCount() > 99">
<ng-container *ngIf="notificationsCount() > 99">+99</ng-container>
<ng-container *ngIf="notificationsCount() <= 99">{{
notificationsCount()
}}</ng-container>
</span>
<button
class="notifications-action"
type="button"
tuiIconButton
appearance="navigation-action"
[attr.aria-expanded]="openNotificationsDropdown"
aria-haspopup="true"
icon="bell"
data-test="notifications-button"
[attr.aria-label]="t('navigation.notifications.title')"></button>
</div>
</tui-hosted-dropdown>
</div>
<div class="separator"></div>
<div class="avatar-holder">
<tui-hosted-dropdown
Expand Down Expand Up @@ -110,6 +137,10 @@
</div>
</div>

<ng-template #notificationsDropdown>
<tg-notifications></tg-notifications>
</ng-template>

<ng-template #userDropdown>
<tg-navigation-user-dropdown
[user]="user"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import { RouterLink } from '@angular/router';
import { TuiButtonModule, TuiHostedDropdownModule } from '@taiga-ui/core';
import { CommonModule } from '@angular/common';
import { TranslocoDirective, TranslocoPipe } from '@ngneat/transloco';
import { NotificationsComponent } from './notifications/notifications.component';
import { authFeature } from '~/app/modules/auth/data-access/+state/reducers/auth.reducer';
@Component({
selector: 'tg-navigation',
templateUrl: './navigation.component.html',
Expand All @@ -37,14 +39,19 @@ import { TranslocoDirective, TranslocoPipe } from '@ngneat/transloco';
NavigationUserDropdownComponent,
NavigationProjectsComponent,
TranslocoPipe,
NotificationsComponent,
],
})
export class NavigationComponent {
public openProjectsDropdown = false;
public openUserDropdown = false;
public openNotificationsDropdown = false;

public user$ = this.store.select(selectUser);
public logged$ = this.user$.pipe(map(() => this.authService.isLogged()));
public notificationsCount = this.store.selectSignal(
authFeature.unreadNotificationsCount
);

constructor(
private store: Store,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
Copyright (c) 2023-present Kaleidos INC
*/

:host {
&:not(:last-child) {
border-block-end: 1px solid var(--color-gray20);
padding-block-end: var(--spacing-12);
}

&::ng-deep .username {
color: var(--color-secondary);
}
}

.type {
color: var(--color-gray80);
}

.date {
color: var(--color-gray70);
font-size: var(--font-size-small);
}

.notification-inner {
display: flex;
gap: var(--spacing-8);
inline-size: 100%;
padding: var(--spacing-8);
}

p {
line-height: 21px;
margin: 0;
}

a {
&:hover {
text-decoration: underline;
}
}

.unread .notification-inner {
background-color: var(--color-gray10);
border-radius: 4px;
position: relative;

&::before {
aspect-ratio: 1;
background-color: var(--color-secondary);
block-size: 8px;
border-radius: 50%;
content: "";
inset-block-start: var(--spacing-12);
inset-inline-end: var(--spacing-8);
position: absolute;
}
}
Loading

0 comments on commit 8de2a94

Please sign in to comment.