Skip to content

Commit 4a3fd0f

Browse files
committed
MOBILE-4574 badges: Support links to badges by hash
1 parent e49f10e commit 4a3fd0f

File tree

9 files changed

+218
-31
lines changed

9 files changed

+218
-31
lines changed
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// (C) Copyright 2015 Moodle Pty Ltd.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import { NgModule } from '@angular/core';
16+
import { RouterModule, Routes } from '@angular/router';
17+
18+
import { AddonBadgesIssuedBadgePage } from './pages/issued-badge/issued-badge';
19+
20+
const routes: Routes = [
21+
{
22+
path: ':badgeHash',
23+
component: AddonBadgesIssuedBadgePage,
24+
data: { usesSwipeNavigation: false },
25+
},
26+
];
27+
28+
@NgModule({
29+
imports: [
30+
RouterModule.forChild(routes),
31+
],
32+
})
33+
export class AddonBadgeLazyModule {}

src/addons/badges/badges-lazy.module.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ const mobileRoutes: Routes = [
3131
{
3232
path: ':badgeHash',
3333
component: AddonBadgesIssuedBadgePage,
34+
data: { usesSwipeNavigation: true },
3435
},
3536
];
3637

@@ -42,6 +43,7 @@ const tabletRoutes: Routes = [
4243
{
4344
path: ':badgeHash',
4445
component: AddonBadgesIssuedBadgePage,
46+
data: { usesSwipeNavigation: true },
4547
},
4648
],
4749
},
@@ -59,7 +61,6 @@ const routes: Routes = [
5961
],
6062
declarations: [
6163
AddonBadgesUserBadgesPage,
62-
AddonBadgesIssuedBadgePage,
6364
],
6465
})
6566
export class AddonBadgesLazyModule {}

src/addons/badges/badges.module.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ export async function getBadgesServices(): Promise<Type<unknown>[]> {
4040
}
4141

4242
const mainMenuRoutes: Routes = [
43+
{
44+
path: 'badge',
45+
loadChildren: () => import('./badge-lazy.module').then(m => m.AddonBadgeLazyModule),
46+
},
4347
{
4448
path: 'badges',
4549
loadChildren: () => import('./badges-lazy.module').then(m => m.AddonBadgesLazyModule),

src/addons/badges/pages/issued-badge/issued-badge.html

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,14 @@ <h1 *ngIf="!badge">{{ 'addon.badges.badges' | translate }}</h1>
2525
</ion-item>
2626
</ion-item-group>
2727

28-
<ion-item *ngIf="user">
29-
<ion-label>
30-
<p class="item-heading">
31-
{{ 'addon.badges.awardedto' | translate: {$a: user.fullname } }}
32-
</p>
33-
</ion-label>
34-
</ion-item>
35-
3628
<ng-container *ngIf="badge">
29+
<ion-item>
30+
<ion-label>
31+
<p class="item-heading">
32+
{{ 'addon.badges.awardedto' | translate: {$a: badge.recipientfullname } }}
33+
</p>
34+
</ion-label>
35+
</ion-item>
3736
<ion-item-group>
3837
<ion-item-divider>
3938
<ion-label>

src/addons/badges/pages/issued-badge/issued-badge.ts

Lines changed: 38 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { Component, OnDestroy, OnInit } from '@angular/core';
1616
import { CoreTimeUtils } from '@services/utils/time';
1717
import { CoreDomUtils } from '@services/utils/dom';
1818
import { CoreSites } from '@services/sites';
19-
import { CoreUser, CoreUserProfile } from '@features/user/services/user';
19+
import { CoreUser } from '@features/user/services/user';
2020
import { AddonBadges, AddonBadgesUserBadge } from '../../services/badges';
2121
import { CoreUtils } from '@services/utils/utils';
2222
import { CoreCourses, CoreEnrolledCourseData } from '@features/courses/services/courses';
@@ -27,13 +27,18 @@ import { AddonBadgesUserBadgesSource } from '@addons/badges/classes/user-badges-
2727
import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker';
2828
import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics';
2929
import { CoreTime } from '@singletons/time';
30+
import { CoreSharedModule } from '@/core/shared.module';
3031

3132
/**
3233
* Page that displays the list of calendar events.
3334
*/
3435
@Component({
3536
selector: 'page-addon-badges-issued-badge',
3637
templateUrl: 'issued-badge.html',
38+
standalone: true,
39+
imports: [
40+
CoreSharedModule,
41+
],
3742
})
3843
export class AddonBadgesIssuedBadgePage implements OnInit, OnDestroy {
3944

@@ -42,10 +47,9 @@ export class AddonBadgesIssuedBadgePage implements OnInit, OnDestroy {
4247
protected logView: (badge: AddonBadgesUserBadge) => void;
4348

4449
courseId = 0;
45-
user?: CoreUserProfile;
4650
course?: CoreEnrolledCourseData;
4751
badge?: AddonBadgesUserBadge;
48-
badges: CoreSwipeNavigationItemsManager;
52+
badges?: CoreSwipeNavigationItemsManager;
4953
badgeLoaded = false;
5054
currentTime = 0;
5155

@@ -54,12 +58,15 @@ export class AddonBadgesIssuedBadgePage implements OnInit, OnDestroy {
5458
this.userId = CoreNavigator.getRouteNumberParam('userId') || CoreSites.getRequiredCurrentSite().getUserId();
5559
this.badgeHash = CoreNavigator.getRouteParam('badgeHash') || '';
5660

57-
const source = CoreRoutedItemsManagerSourcesTracker.getOrCreateSource(
58-
AddonBadgesUserBadgesSource,
59-
[this.courseId, this.userId],
60-
);
61+
const routeData = CoreNavigator.getRouteData(this.route);
62+
if (routeData.usesSwipeNavigation) {
63+
const source = CoreRoutedItemsManagerSourcesTracker.getOrCreateSource(
64+
AddonBadgesUserBadgesSource,
65+
[this.courseId, this.userId],
66+
);
6167

62-
this.badges = new CoreSwipeNavigationItemsManager(source);
68+
this.badges = new CoreSwipeNavigationItemsManager(source);
69+
}
6370

6471
this.logView = CoreTime.once((badge) => {
6572
CoreAnalytics.logEvent({
@@ -80,14 +87,14 @@ export class AddonBadgesIssuedBadgePage implements OnInit, OnDestroy {
8087
this.badgeLoaded = true;
8188
});
8289

83-
this.badges.start();
90+
this.badges?.start();
8491
}
8592

8693
/**
8794
* @inheritdoc
8895
*/
8996
ngOnDestroy(): void {
90-
this.badges.destroy();
97+
this.badges?.destroy();
9198
}
9299

93100
/**
@@ -96,16 +103,29 @@ export class AddonBadgesIssuedBadgePage implements OnInit, OnDestroy {
96103
* @returns Promise resolved when done.
97104
*/
98105
async fetchIssuedBadge(): Promise<void> {
106+
const site = CoreSites.getRequiredCurrentSite();
99107
this.currentTime = CoreTimeUtils.timestamp();
100108

101-
this.user = await CoreUser.getProfile(this.userId, this.courseId, true);
102-
103109
try {
110+
// Search the badge in the user badges.
104111
const badges = await AddonBadges.getUserBadges(this.courseId, this.userId);
105-
const badge = badges.find((badge) => this.badgeHash == badge.uniquehash);
112+
let badge = badges.find((badge) => this.badgeHash == badge.uniquehash);
106113

107-
if (!badge) {
108-
return;
114+
if (badge) {
115+
if (!site.isVersionGreaterEqualThan('4.5')) {
116+
// Web service does not return the name of the recipient.
117+
const user = await CoreUser.getProfile(this.userId, this.courseId, true);
118+
badge.recipientfullname = user.fullname;
119+
}
120+
} else {
121+
// The badge is awarded to another user, try to fetch the badge by hash.
122+
if (site.isVersionGreaterEqualThan('4.5')) {
123+
badge = await AddonBadges.getUserBadgeByHash(this.badgeHash);
124+
}
125+
if (!badge) {
126+
// Should never happen. The app opens the badge in the browser if it can't be fetched.
127+
throw new Error('Error getting badge data.');
128+
}
109129
}
110130

111131
this.badge = badge;
@@ -130,9 +150,10 @@ export class AddonBadgesIssuedBadgePage implements OnInit, OnDestroy {
130150
* @param refresher Refresher.
131151
*/
132152
async refreshBadges(refresher?: HTMLIonRefresherElement): Promise<void> {
133-
await CoreUtils.ignoreErrors(Promise.all([
153+
await CoreUtils.allPromisesIgnoringErrors([
134154
AddonBadges.invalidateUserBadges(this.courseId, this.userId),
135-
]));
155+
AddonBadges.invalidateUserBadgeByHash(this.badgeHash),
156+
]);
136157

137158
await CoreUtils.ignoreErrors(Promise.all([
138159
this.fetchIssuedBadge(),
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
// (C) Copyright 2015 Moodle Pty Ltd.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import { Injectable } from '@angular/core';
16+
17+
import { makeSingleton } from '@singletons';
18+
import { AddonBadges } from './badges';
19+
import { CoreSites } from '@services/sites';
20+
21+
/**
22+
* Helper service that provides some features for badges.
23+
*/
24+
@Injectable({ providedIn: 'root' })
25+
export class AddonBadgesHelperProvider {
26+
27+
/**
28+
* Return whether the badge can be opened in the app.
29+
*
30+
* @param badgeHash Badge hash.
31+
* @param siteId Site ID. If not defined, current site.
32+
* @returns Whether the badge can be opened in the app.
33+
*/
34+
async canOpenBadge(badgeHash: string, siteId?: string): Promise<boolean> {
35+
if (!AddonBadges.isPluginEnabled(siteId)) {
36+
return false;
37+
}
38+
39+
const site = await CoreSites.getSite(siteId);
40+
41+
if (site.isVersionGreaterEqualThan('4.5')) {
42+
// The WS to fetch a badge by hash is available and it returns the name of the recipient.
43+
return true;
44+
}
45+
46+
// Open in app if badge is one of the user badges.
47+
const badges = await AddonBadges.getUserBadges(0, site.getUserId());
48+
const badge = badges.find((badge) => badgeHash == badge.uniquehash);
49+
50+
return badge !== undefined;
51+
}
52+
53+
}
54+
55+
export const AddonBadgesHelper = makeSingleton(AddonBadgesHelperProvider);

src/addons/badges/services/badges.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,58 @@ export class AddonBadgesProvider {
106106
await site.invalidateWsCacheForKey(this.getBadgesCacheKey(courseId, userId));
107107
}
108108

109+
/**
110+
* Get the cache key for the get badge by hash WS call.
111+
*
112+
* @param hash Badge issued hash.
113+
* @returns Cache key.
114+
*/
115+
protected getUserBadgeByHashCacheKey(hash: string): string {
116+
return ROOT_CACHE_KEY + 'badge:' + hash;
117+
}
118+
119+
/**
120+
* Get issued badge by hash.
121+
*
122+
* @param hash Badge issued hash.
123+
* @returns Promise to be resolved when the badge is retrieved.
124+
* @since 4.5 with the recpient name, 4.3 without the recipient name.
125+
*/
126+
async getUserBadgeByHash(hash: string, siteId?: string): Promise<AddonBadgesUserBadge> {
127+
const site = await CoreSites.getSite(siteId);
128+
const data: AddonBadgesGetUserBadgeByHashWSParams = {
129+
hash,
130+
};
131+
const preSets = {
132+
cacheKey: this.getUserBadgeByHashCacheKey(hash),
133+
updateFrequency: CoreSite.FREQUENCY_RARELY,
134+
};
135+
136+
const response = await site.read<AddonBadgesGetUserBadgeByHashWSResponse>(
137+
'core_badges_get_user_badge_by_hash',
138+
data,
139+
preSets,
140+
);
141+
if (!response || !response.badge?.[0]) {
142+
throw new CoreError('Invalid badge response');
143+
}
144+
145+
return response.badge[0];
146+
}
147+
148+
/**
149+
* Invalidate get badge by hash WS call.
150+
*
151+
* @param hash Badge issued hash.
152+
* @param siteId Site ID. If not defined, current site.
153+
* @returns Promise resolved when data is invalidated.
154+
*/
155+
async invalidateUserBadgeByHash(hash: string, siteId?: string): Promise<void> {
156+
const site = await CoreSites.getSite(siteId);
157+
158+
await site.invalidateWsCacheForKey(this.getUserBadgeByHashCacheKey(hash));
159+
}
160+
109161
}
110162

111163
export const AddonBadges = makeSingleton(AddonBadgesProvider);
@@ -167,6 +219,8 @@ export type AddonBadgesUserBadge = {
167219
dateissued: number; // Date issued.
168220
dateexpire: number; // Date expire.
169221
visible?: number; // Visible.
222+
recipientid?: number; // @since 4.5. Id of the awarded user.
223+
recipientfullname?: string; // @since 4.5. Full name of the awarded user.
170224
email?: string; // @since 3.6. User email.
171225
version?: string; // @since 3.6. Version.
172226
language?: string; // @since 3.6. Language.
@@ -211,3 +265,18 @@ export type AddonBadgesUserBadge = {
211265
type?: number; // Type.
212266
}[];
213267
};
268+
269+
/**
270+
* Params of core_badges_get_user_badge_by_hash WS.
271+
*/
272+
type AddonBadgesGetUserBadgeByHashWSParams = {
273+
hash: string; // Badge issued hash.
274+
};
275+
276+
/**
277+
* Data returned by core_badges_get_user_badge_by_hash WS.
278+
*/
279+
type AddonBadgesGetUserBadgeByHashWSResponse = {
280+
badge: AddonBadgesUserBadge[];
281+
warnings?: CoreWSExternalWarning[];
282+
};

src/addons/badges/services/handlers/badge-link.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import { CoreContentLinksHandlerBase } from '@features/contentlinks/classes/base
1818
import { CoreContentLinksAction } from '@features/contentlinks/services/contentlinks-delegate';
1919
import { CoreNavigator } from '@services/navigator';
2020
import { makeSingleton } from '@singletons';
21-
import { AddonBadges } from '../badges';
21+
import { AddonBadgesHelper } from '../badges-helper';
2222

2323
/**
2424
* Handler to treat links to user participants page.
@@ -36,16 +36,16 @@ export class AddonBadgesBadgeLinkHandlerService extends CoreContentLinksHandlerB
3636

3737
return [{
3838
action: async (siteId: string): Promise<void> => {
39-
await CoreNavigator.navigateToSitePath(`/badges/${params.hash}`, { siteId });
39+
await CoreNavigator.navigateToSitePath(`/badge/${params.hash}`, { siteId });
4040
},
4141
}];
4242
}
4343

4444
/**
4545
* @inheritdoc
4646
*/
47-
isEnabled(siteId: string): Promise<boolean> {
48-
return AddonBadges.isPluginEnabled(siteId);
47+
async isEnabled(siteId: string, url: string, params: Record<string, string>): Promise<boolean> {
48+
return AddonBadgesHelper.canOpenBadge(params.hash, siteId);
4949
}
5050

5151
}

0 commit comments

Comments
 (0)