Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update Avatar and Username example in iframe playground #26

Merged
merged 4 commits into from
Mar 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading