Skip to content

Commit 8ee3464

Browse files
committed
Add more fields to User model
- Change `away` flag to `status` - Now tracks online, offline, and away statuses - Add color for username display color - Add profile and isFetchingProfile fields for the user tooltip information - Cache the fetched profile data in user list store - Add ability to parse previous NICK format for username and UID - Add OnlineUser list component
1 parent f1d85b6 commit 8ee3464

16 files changed

Lines changed: 290 additions & 55 deletions

File tree

src/App.vue

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
<template>
22
<BApp>
3-
<h1>Connection status: {{ irc.connectionStatus }}</h1>
4-
53
<ChatApp v-if="irc.isInitialized" />
64
<LoginForm v-else @login="irc.signIn" />
75
</BApp>
@@ -12,6 +10,11 @@
1210
import LoginForm from '#components/LoginForm.vue';
1311
import { useIrcStore } from '#stores';
1412
import { BApp } from 'bootstrap-vue-next';
13+
import { onMounted } from 'vue';
1514
1615
const irc = useIrcStore();
16+
17+
onMounted(() => {
18+
irc.autoSignIn();
19+
});
1720
</script>

src/components/ChatApp.vue

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,34 @@
11
<template>
22
<DraggableWindow>
33
<template v-slot:main>
4+
<OnlineUsers />
45
<Transition name="fade"> </Transition>
56
</template>
67
</DraggableWindow>
78
</template>
89

910
<script setup lang="ts">
1011
import DraggableWindow from './DraggableWindow.vue';
12+
import OnlineUsers from './user/OnlineUsers.vue';
13+
import { useChannelStore, useIrcStore } from '#stores';
14+
import { DEFAULT_CHANNELS } from '#constants';
15+
import { ref, watch } from 'vue';
1116
// TODO: Re-implement resizing tracking
17+
18+
const irc = useIrcStore();
19+
const channels = useChannelStore();
20+
const joined = ref(false);
21+
22+
watch([joined, () => irc.connectionStatus], ([hasJoined, connectionStatus]) => {
23+
if (hasJoined || connectionStatus !== 'connected') {
24+
return;
25+
}
26+
27+
for (const channel of DEFAULT_CHANNELS) {
28+
channels.joinChannel(channel);
29+
}
30+
channels.changeActiveChannel(DEFAULT_CHANNELS[0]);
31+
});
1232
</script>
1333

1434
<style scoped>

src/components/LoginForm.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@
6666
}
6767
6868
function anonLogin() {
69-
emit('login', { username: 'anonymous', uid: '0', remember: false });
69+
emit('login', { username: 'Anonymous', uid: '0', remember: false });
7070
}
7171
</script>
7272
<style scoped>

src/components/channel/ChannelMessages.vue

Whitespace-only changes.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<template>
2+
<ul>
3+
<li v-for="user in connectedUsers" :key="user.username">
4+
<UsernameDisplay :username="user.username" />
5+
</li>
6+
</ul>
7+
</template>
8+
<script setup lang="ts">
9+
import { useUserListStore } from '#stores';
10+
import { computed } from 'vue';
11+
import UsernameDisplay from './UsernameDisplay.vue';
12+
13+
const userList = useUserListStore();
14+
const connectedUsers = computed(() =>
15+
userList.users
16+
.filter((user) => user.status !== 'offline')
17+
.sort((a, b) => a.username.localeCompare(b.username)),
18+
);
19+
</script>
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
<template>
2+
<BPopover click @show="onShow">
3+
<template #target>
4+
<div class="username" :style="{ color: usernameColor }">
5+
<span
6+
v-if="user.status !== 'online'"
7+
class="indicator"
8+
:class="{
9+
'indicator--away': user.status === 'away',
10+
'indicator--offline': user.status === 'offline',
11+
}"
12+
>●</span
13+
>
14+
{{ user.username }}
15+
</div>
16+
</template>
17+
18+
<template v-if="user.isFetchingProfile || !user.profile">Fetching player profile...</template>
19+
<template v-else>
20+
<img :src="user.profile.avatar ?? defaultAvatar" alt="" />
21+
22+
<a v-if="user.profile.link" :href="user.profile.link" target="_blank">{{ user.username }}</a>
23+
<span v-else>{{ user.username }}</span>
24+
25+
<dl>
26+
<dt>Rank</dt>
27+
<dd v-if="user.profile.rank">#{{ user.profile.rank }}</dd>
28+
<dd v-else>Unranked</dd>
29+
30+
<dt>Roles</dt>
31+
<dd>{{ user.profile.roles.join(', ') }}</dd>
32+
</dl>
33+
34+
<section v-if="user.profile.description" v-html="user.profile.description" />
35+
<p v-else>User has not added a description to their profile.</p>
36+
</template>
37+
</BPopover>
38+
</template>
39+
40+
<script setup lang="ts">
41+
import defaultAvatar from '#assets/default-avatar.svg';
42+
import { useUserListStore } from '#stores';
43+
import { BPopover } from 'bootstrap-vue-next';
44+
import { computed } from 'vue';
45+
46+
const props = defineProps<{
47+
username: string;
48+
}>();
49+
const userList = useUserListStore();
50+
const user = computed(() => userList.getUser(props.username));
51+
const usernameColor = computed(() => user.value.color);
52+
53+
function onShow() {
54+
userList.loadProfile(props.username);
55+
}
56+
</script>
57+
58+
<style scoped>
59+
.username {
60+
display: inline-block;
61+
}
62+
.indicator--away {
63+
color: yellow;
64+
}
65+
.indicator--offline {
66+
color: gray;
67+
}
68+
</style>

src/constants/users.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,27 @@ import type { User } from '#models';
33
export const ANONYMOUS_USER: User = {
44
username: 'anonymous',
55
uid: '0',
6-
nicks: ['anonymous^0'],
7-
away: false,
6+
nicks: new Set([]),
7+
status: 'offline',
88
awayReason: '',
9+
color: '#ffffff',
10+
profile: {
11+
avatar: null,
12+
rank: null,
13+
description: null,
14+
roles: ['Player'],
15+
link: null,
16+
},
17+
isFetchingProfile: false,
18+
};
19+
20+
/**
21+
* These are currently taken from the Discord/Slack roles
22+
* Eventually, this will be replaced with an API call
23+
*/
24+
export const USER_ROLES: Record<string, string[]> = {
25+
Developer: ['LFP6', 'Ahalb', 'ElNando888', 'jnicol', 'MasterStormer'],
26+
Scientist: ['rhiju', 'dosoonkim'],
27+
Staff: ['LFP6', 'Omei', 'rhiju'],
28+
Moderator: ['Hoglahoo', 'LFP6', 'Omei'],
929
};

src/models/message.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
1-
import type { User } from './user';
2-
31
export type MessageType = 'message' | 'action' | 'system';
42

53
export interface Message {
64
time: Date;
75
starred: boolean;
86
message: string;
97
target: string;
10-
user: User;
8+
nick: string;
119
type: MessageType;
1210
tags: Record<string, string>;
1311
}

src/models/user.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,25 @@
1+
export type UserStatus = 'offline' | 'online' | 'away';
2+
3+
export interface UserProfile {
4+
avatar: string | null;
5+
rank: number | null;
6+
/** HTML formatted */
7+
description: string | null;
8+
roles: string[];
9+
/** Profile page link */
10+
link: string | null;
11+
}
12+
113
export interface User {
214
username: string;
315
/** User ID */
416
uid: string;
517
/** Tracks multiple opened clients for the same user */
618
nicks: Set<string>;
7-
away: boolean;
19+
status: UserStatus;
820
awayReason: string;
21+
/** Hex color value */
22+
color: string;
23+
profile: UserProfile | null;
24+
isFetchingProfile: boolean;
925
}

src/services/user.ts

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,40 @@
1-
export async function getUserInfo(uid: string | number) {
2-
const response = await fetch(`https://eternagame.org/get/?type=user&uid=${uid}`, {
3-
method: 'GET',
4-
});
5-
return response.json();
1+
import { USER_ROLES } from '#constants';
2+
import type { UserProfile } from '#models';
3+
import log from 'loglevel';
4+
5+
export async function getUserProfile(uid: string): Promise<UserProfile> {
6+
let user: Record<string, unknown> | null = null;
7+
8+
if (uid !== '0' && uid !== 'anon') {
9+
const response = await fetch(`https://eternagame.org/get/?type=user&uid=${uid}`, {
10+
method: 'GET',
11+
});
12+
const result = await response.json().catch((err) => {
13+
log.error(err);
14+
return null;
15+
});
16+
user = result?.data?.user ?? null;
17+
}
18+
19+
const roles: string[] = [];
20+
if (user && typeof user.name === 'string') {
21+
Object.entries(USER_ROLES).forEach(([role, usernames]) => {
22+
if (
23+
usernames.some(
24+
(username) => username.toLocaleLowerCase() === (user.name as string).toLocaleLowerCase(),
25+
)
26+
) {
27+
roles.push(role);
28+
}
29+
});
30+
}
31+
roles.push('Player');
32+
33+
return {
34+
avatar: user?.picture ? `https://eternagame.org/${user.picture}` : null,
35+
rank: (user?.rank ?? null) as number | null,
36+
description: typeof user?.Profile === 'string' && user.Profile.length > 0 ? user.Profile : null,
37+
roles,
38+
link: user?.uid ? `https://eternagame.org/players/${user.uid}` : null,
39+
};
640
}

0 commit comments

Comments
 (0)