davidcr01 / WordlePlus

Repository to store all the documentation, files and structure of my Final Degree Project (TFG in Spanish). The main goal is to develop, as full-stack web developer, a remodel of the Wordle game, including more features and functionalities using Ionic, Django REST Api and PostgreSQL.
1 stars 0 forks source link

Notification ibox (S4) #27

Closed davidcr01 closed 1 year ago

davidcr01 commented 1 year ago

Description

As a player, is necessary to implement an inbox where some important messages will be stored.

Tasks

davidcr01 commented 1 year ago

Update Report

Backend

The model and serializer of the Notifications are the following:

class Notifications(models.Model):
    player = models.ForeignKey(Player, on_delete=models.CASCADE, related_name='notifications')
    text = models.CharField(max_length=200)
    link = models.URLField(blank=True)
    timestamp = models.DateTimeField(auto_now_add=True)

Notice that the link could be blank. This option is controlled by the blank=True parameter.

class NotificationsSerializer(serializers.ModelSerializer):
    class Meta:
        model = Notifications
        fields = ['text', 'link']

And the related viewset is:

class NotificationsViewSet(viewsets.ModelViewSet):
    queryset = Notifications.objects.all()
    serializer_class = NotificationsSerializer
    permission_classes = [permissions.IsAuthenticated, IsOwnerPermission]

    def list(self, request):
        limit = int(request.query_params.get('limit', 10))
        player = getattr(request.user, 'player', None)

        if not player:
            return Response({'error': 'Player not found'}, status=404)

        notifications = self.queryset.filter(player=player).order_by('-timestamp')[:limit]
        serializer = self.serializer_class(notifications, many=True)

        return Response(serializer.data)

    def create(self, request):
        player = getattr(request.user, 'player', None)

        if not player:
            return Response({'error': 'Player not found'}, status=404)

        serializer = self.serializer_class(data=request.data)
        serializer.is_valid(raise_exception=True)
        serializer.save(player=player)
        return Response(serializer.data, status=201)

Notice that, instead of passing the player ID as a parameter, we are getting the player from the request.user value. This strategy is also added to the ClassicWordleViewSet. This view filters the notifications by the date and limits the query to 10 or to the specified parameter limit.

The testing of this new API is:

Posting a new notification attaching the access token: image

davidcr01 commented 1 year ago

Update Report

Frontend

To implement this new feature, a new component and service have been created:

<ion-content>
  <h1>Notifications</h1>
  <ion-list>
    <ion-item *ngFor="let notification of notifications">
      <a [href]="notification.link">{{ notification.text }}</a>
    </ion-item>
  </ion-list>
</ion-content>

It iterates though the list of notifications.

On the other hand, in the TS logic:

import { Component, OnInit } from '@angular/core';
import { PopoverController } from '@ionic/angular';
import { NotificationService } from 'src/app/services/notification.service';

@Component({
  selector: 'app-notifications-popover',
  templateUrl: './notifications-popover.component.html',
  styleUrls: ['./notifications-popover.component.scss'],
})
export class NotificationsPopoverComponent implements OnInit {

  notifications: any[] = [];

  constructor(
    private notificationService: NotificationService
  ) {}

  ngOnInit() {
    this.loadNotifications();
  }

  loadNotifications() {
    this.notificationService.getNotifications().then((notifications: any[]) => {
      this.notifications = notifications || []; 
    });
  }

}

It defines the notifications list and fill it using the NotificationService.

getNotifications(): Promise<any[]> {
    return new Promise(async (resolve) => {
      if (this.notifications.length > 0) {
        resolve(this.notifications);
      } else {
        const storedNotifications = await this.storageService.getNotifications();
        console.log(storedNotifications);
        if (storedNotifications) {
          this.notifications = storedNotifications;
          resolve(this.notifications);
        } else {
          (await this.apiService.getNotifications()).subscribe((apiNotifications: any[]) => {
            this.notifications = apiNotifications || [];
            this.storageService.setNotifications(this.notifications);
            resolve(this.notifications);
          });
        }
      }
    });
  }
// Request to the API to get the notifications
  async refreshNotifications() {
    (await this.apiService.getNotifications()).subscribe((apiNotifications: any[]) => {
        this.notifications = apiNotifications || [];
        this.storageService.setNotifications(this.notifications);
        return this.notifications;
      });
  }
async addNotification(notification: { text: string; link?: string }): Promise<void> {
    const newNotification = { text: notification.text, link: notification.link || '' };
    (await this.apiService.addNotification(newNotification)).subscribe(
      (response) => {
        console.log('Notification added successfully', response);
      },
      (error) => {
        console.log('Notification could not be added', error);
    }
    );

    const storedNotifications = await this.storageService.getNotifications() || [];
    const updatedNotifications = [...storedNotifications, newNotification];
    await this.storageService.setNotifications(updatedNotifications);
  }

This service uses the api.service, which has two new methods to get and post notifications. It uses the information passed by parameter and attach the access token:

async getNotifications(limit: number = 5) {
        const url = `${this.baseURL}/api/notifications/?limit=${limit}`;
        const accessToken = await this.storageService.getAccessToken();
        if (!accessToken) {
            return throwError('Access token not found');
        }
        const decryptedToken = this.encryptionService.decryptData(accessToken);
        const headers = new HttpHeaders({
            Authorization: `Token ${decryptedToken}`,
            'Content-Type': 'application/json'
        });
        return this.http.get(url, {headers});
    }

    async addNotification(notification: { text: string, link: string }): Promise<Observable<any>> {
        let url = `${this.baseURL}/api/notifications/`;
        const accessToken = this.storageService.getAccessToken();
        if (!accessToken) {
            return throwError('Access token not found');
        }
        const decryptedToken = this.encryptionService.decryptData(await accessToken);
        const headers = new HttpHeaders({
            Authorization: `Token ${decryptedToken}`,
            'Content-Type': 'application/json'
        });

        return this.http.post(url, notification, { headers });
    }
davidcr01 commented 1 year ago

Update Report

Updating the main page

Before this issue, the user had to go manually to the main page when a game was finished. Now, as the platform redirects it automatically, is necessary to implement a new logic:

In the tab1 page (main page), this new logic has been added:

// Change background img depending on the width
  async ngOnInit() {
    if (window.innerWidth <= 767) {
      this.backgroundImage = '../../assets/background_wordle_vertical.png';
    } else {
      this.backgroundImage = '../../assets/background_wordle_horizontal.png';
    }

    // Only fetchs the avatar if necessary
    const storedAvatarUrl = await this.storageService.getAvatarUrl();
    if (storedAvatarUrl) {
      this.avatarImage = storedAvatarUrl;
    } else {
      await this.loadAvatarImage();
    }

    // Optional param to update the player info: useful when
    // finishing a game
    this.route.queryParams.subscribe(async params => {
      const refresh = params['refresh'];
      if (refresh === 'true') {
        await this.ionViewWillEnter();
      }
    }); 
  }

💡 Notice that a new extra parameter has been added to the URL. This parameter indicates that is necessary to update the information on the page. If true, it calls the ionViewWillEnter method which retrieves all the related information and refreshes the notifications.

When finishing the wordle game, this code redirects the user:

setTimeout(() => {
        this.router.navigate(['/tabs/main'], { queryParams: { refresh: 'true' } });
      }, 3000)

Result

This video shows how the notifications are updated when the user is redirected to the main page and a new notification is created and added.

https://github.com/davidcr01/WordlePlus/assets/72193239/89dedc08-f915-439d-9cba-483a61f7d728