Skip to content

Commit

Permalink
Update Avatar and Username example in iframe playground (#26)
Browse files Browse the repository at this point in the history
* Update pattern for fetching avatars and usernames

* Wrap user id in Math.abs before modulo

* Patch usage in react-colyseus

* cleanup iframe plaground
  • Loading branch information
matthova authored Mar 11, 2024
1 parent 398f14c commit 4a9d34c
Show file tree
Hide file tree
Showing 14 changed files with 138 additions and 158 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {authStore} from '../stores/authStore';
import discordSdk from '../discordSdk';
import {fetchGuildsUserAvatarAndNickname} from '../utils/fetchGuildsUserAvatarAndNickname';
import {type Types} from '@discord/embedded-app-sdk';
import {IGuildsMembersRead} from '../types';

export const start = async () => {
const {user} = authStore.getState();
Expand Down Expand Up @@ -61,7 +61,14 @@ export const start = async () => {
});

// Get guild specific nickname and avatar, and fallback to user name and avatar
const guildsReadInfo = await fetchGuildsUserAvatarAndNickname(authResponse);
const guildMember = await fetch(`/discord/api/users/@me/guilds/${discordSdk.guildId}/member`, {
method: 'get',
headers: {Authorization: `Bearer ${access_token}`},
})
.then((j) => j.json<IGuildsMembersRead>())
.catch(() => {
return null;
});

// Done with discord-specific setup

Expand All @@ -71,7 +78,7 @@ export const start = async () => {
...authResponse.user,
id: new URLSearchParams(window.location.search).get('user_id') ?? authResponse.user.id,
},
...guildsReadInfo,
guildMember,
};

authStore.setState({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import discordSdk from '../discordSdk';
import ReactJsonView from '../components/ReactJsonView';
import {DiscordAPI, RequestType} from '../DiscordAPI';
import {authStore} from '../stores/authStore';
import {getUserAvatarUri} from '../utils/getUserAvatarUri';

interface GuildsMembersRead {
roles: string[];
Expand All @@ -26,17 +27,18 @@ interface GuildsMembersRead {

export default function AvatarAndName() {
const auth = authStore.getState();
const [guildsMembersRead, setGuildsMembersRead] = React.useState<GuildsMembersRead | null>(null);
const [guildMember, setGuildMember] = React.useState<GuildsMembersRead | null>(null);

React.useEffect(() => {
if (auth == null) {
return;
}
// We store this in the auth object, but fetching it again to keep relevant patterns in one area
DiscordAPI.request<GuildsMembersRead>(
{method: RequestType.GET, endpoint: `/users/@me/guilds/${discordSdk.guildId}/member`},
auth.access_token
).then((reply) => {
setGuildsMembersRead(reply);
setGuildMember(reply);
});
}, [auth]);

Expand All @@ -47,24 +49,26 @@ export default function AvatarAndName() {
// Note: instead of doing this here, your app's server could retrieve this
// data by using the user's OAuth token

// Get the user's profile avatar uri
// If none available, use a default avatar
const userAvatarSrc = auth.user.avatar
? `https://cdn.discordapp.com/avatars/${auth.user.id}/${auth.user.avatar}.png?size=256`
: `https://cdn.discordapp.com/embed/avatars/${parseInt(auth.user.discriminator) % 5}.png`;
const username = `${auth.user.username}#${auth.user.discriminator}`;
const userAvatarUri = getUserAvatarUri({
userId: auth.user.id,
avatarHash: auth.user.avatar,
guildId: discordSdk.guildId,
guildAvatarHash: auth.guildMember?.avatar,
});

// Get the user's guild-specific avatar uri
// If none, fall back to the user profile avatar
// If no main avatar, use a default avatar
const guildAvatarSrc = guildsMembersRead?.avatar
? `https://cdn.discordapp.com/guilds/${discordSdk.guildId}/users/${auth.user.id}/avatars/${guildsMembersRead.avatar}.png?size=256`
: auth.user.avatar
? `https://cdn.discordapp.com/avatars/${auth.user.id}/${auth.user.avatar}.png?size=256`
: `https://cdn.discordapp.com/embed/avatars/${parseInt(auth.user.discriminator) % 5}.png`;
const guildAvatarSrc = getUserAvatarUri({
userId: auth.user.id,
avatarHash: auth.user.avatar,
guildId: discordSdk.guildId,
guildAvatarHash: guildMember?.avatar,
});

// Get the user's guild nickname. If none set, use profile "name#discriminator"
const guildNickname = guildsMembersRead?.nick ?? `${auth.user.username}${auth.user.discriminator}`;
// Get the user's guild nickname. If none set, fall back to global_name, or username
// Note - this name is note guaranteed to be unique
const name = guildMember?.nick ?? auth.user.global_name ?? auth.user.username;

return (
<div style={{padding: 32, overflowX: 'auto'}}>
Expand All @@ -87,31 +91,32 @@ export default function AvatarAndName() {
<br />
<br />
<div>
<p>User avatar and nickname</p>
<img alt="avatar" src={userAvatarSrc} />
<p>User Avatar url: "{userAvatarSrc}"</p>
<p>Username: "{username}"</p>
<p>User avatar, global name, and username</p>
<img alt="avatar" src={userAvatarUri} />
<p>User Avatar url: "{userAvatarUri}"</p>
<p>Global Name: "{auth.user.global_name}"</p>
<p>Unique username: "{auth.user.username}"</p>
</div>
<br />
<br />
<div>
<p>Guild-specific user avatar and nickname</p>
{guildsMembersRead == null ? (
{guildMember == null ? (
<p>...loading</p>
) : (
<>
<img alt="avatar" src={guildAvatarSrc} />
<p>Guild Member Avatar url: "{guildAvatarSrc}"</p>
<p>Guild nickname: "{guildNickname}"</p>
<p>Guild nickname: "{name}"</p>
</>
)}
</div>
</div>
{guildsMembersRead == null ? null : (
{guildMember == null ? null : (
<>
<br />
<div>API response from {`/api/users/@me/guilds/${discordSdk.guildId}/member`}</div>
<ReactJsonView src={guildsMembersRead} />
<ReactJsonView src={guildMember} />
</>
)}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,5 @@ export const authStore = create<TAuthenticatedContext>(() => ({
icon: null,
description: '',
},
nick: '',
avatarUri: '',
guildMember: null,
}));
8 changes: 1 addition & 7 deletions examples/iframe-playground/packages/client/src/types.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type {CommandResponseTypes} from '@discord/embedded-app-sdk';
export type TAuthenticatedContext = CommandResponseTypes['authenticate'] & IGuildsReadInfo;
export type TAuthenticatedContext = CommandResponseTypes['authenticate'] & {guildMember: IGuildsMembersRead | null};

export interface IGuildsMembersRead {
roles: string[];
Expand All @@ -21,12 +21,6 @@ export interface IGuildsMembersRead {
deaf: boolean;
}

export interface IGuildsReadInfo {
nick: string | null;
/** We're going to take the user info and user's guild info to get *something* for the user's avatar image */
avatarUri: string;
}

// TODO: Can we reuse the existing enum from the SDK package?
// https://app.asana.com/0/1202090529698493/1205406173366737/f
export enum SkuType {
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
interface GetUserAvatarArgs {
userId: string;
avatarHash?: string | null;
guildId?: string | null;
guildAvatarHash?: string | null;
cdn?: string | null;
size?: number;
}
export function getUserAvatarUri({
userId,
avatarHash,
guildId,
guildAvatarHash,
cdn = `https://cdn.discordapp.com`,
size = 256,
}: GetUserAvatarArgs): string {
if (guildId != null && guildAvatarHash != null) {
return `${cdn}/guilds/${guildId}/users/${userId}/avatars/${guildAvatarHash}.png?size=${size}`;
}
if (avatarHash != null) {
return `${cdn}/avatars/${userId}/${avatarHash}.png?size=${size}`;
}

const defaultAvatarIndex = Math.abs(Number(userId) >> 22) % 6;
return `${cdn}/embed/avatars/${defaultAvatarIndex}.png?size=${size}`;
}
2 changes: 1 addition & 1 deletion examples/react-colyseus/packages/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
"private": true,
"version": "0.0.0",
"scripts": {
"@discord/embedded-app-sdk": "workspace:@discord/embedded-app-sdk@*",
"dev": "vite --mode dev",
"build": "tsc && vite build",
"preview": "vite preview"
Expand All @@ -18,6 +17,7 @@
"type-fest": "^4.8.3"
},
"devDependencies": {
"@types/react": "^18.2.64",
"@types/react-dom": "^18.2.7",
"@vitejs/plugin-react": "^1.3.0",
"vite": "^2.9.9"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ import {GAME_NAME} from '../../../server/src/shared/Constants';

import {discordSdk} from '../discordSdk';
import {LoadingScreen} from '../components/LoadingScreen';
import {fetchGuildsUserAvatarAndNickname} from '../utils/fetchGuildsUserAvatarAndNickname';
import {getUserAvatarUri} from '../utils/getUserAvatarUri';

import type {TAuthenticateResponse, TAuthenticatedContext} from '../types';
import type {IGuildsMembersRead, TAuthenticateResponse, TAuthenticatedContext} from '../types';

const AuthenticatedContext = React.createContext<TAuthenticatedContext>({
user: {
Expand All @@ -28,8 +28,7 @@ const AuthenticatedContext = React.createContext<TAuthenticatedContext>({
icon: null,
description: '',
},
nick: '',
avatarUri: '',
guildMember: null,
client: undefined as unknown as Client,
room: undefined as unknown as Room,
});
Expand Down Expand Up @@ -107,7 +106,17 @@ function useAuthenticatedContextSetup() {
});

// Get guild specific nickname and avatar, and fallback to user name and avatar
const guildsReadInfo = await fetchGuildsUserAvatarAndNickname(newAuth);
const guildMember: IGuildsMembersRead | null = await fetch(
`/discord/api/users/@me/guilds/${discordSdk.guildId}/member`,
{
method: 'get',
headers: {Authorization: `Bearer ${access_token}`},
}
)
.then((j) => j.json())
.catch(() => {
return null;
});

// Done with discord-specific setup

Expand All @@ -127,17 +136,31 @@ function useAuthenticatedContextSetup() {
}
}

// Get the user's guild-specific avatar uri
// If none, fall back to the user profile avatar
// If no main avatar, use a default avatar
const avatarUri = getUserAvatarUri({
userId: newAuth.user.id,
avatarHash: newAuth.user.avatar,
guildId: discordSdk.guildId,
guildAvatarHash: guildMember?.avatar,
});

// Get the user's guild nickname. If none set, fall back to global_name, or username
// Note - this name is note guaranteed to be unique
const name = guildMember?.nick ?? newAuth.user.global_name ?? newAuth.user.username;

// The second argument has to include for the room as well as the current player
const newRoom = await client.joinOrCreate<State>(GAME_NAME, {
channelId: discordSdk.channelId,
roomName,
userId: newAuth.user.id,
name: guildsReadInfo.nick,
avatarUri: guildsReadInfo.avatarUri,
name,
avatarUri,
});

// Finally, we construct our authenticatedContext object to be consumed throughout the app
setAuth({...newAuth, ...guildsReadInfo, client, room: newRoom});
setAuth({...newAuth, guildMember, client, room: newRoom});
};

if (!settingUp.current) {
Expand Down
8 changes: 1 addition & 7 deletions examples/react-colyseus/packages/client/src/types.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export interface IColyseus {
room: Room<State>;
client: Client;
}
export type TAuthenticatedContext = TAuthenticateResponse & IGuildsReadInfo & IColyseus;
export type TAuthenticatedContext = TAuthenticateResponse & {guildMember: IGuildsMembersRead | null} & IColyseus;

export interface IGuildsMembersRead {
roles: string[];
Expand All @@ -29,9 +29,3 @@ export interface IGuildsMembersRead {
mute: boolean;
deaf: boolean;
}

export interface IGuildsReadInfo {
nick: string | null;
/** We're going to take the user info and user's guild info to get either the user's guild avatar image, the user's image, or the user's default avatar in the right color */
avatarUri: string;
}
Loading

0 comments on commit 4a9d34c

Please sign in to comment.