Skip to content
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
10 changes: 10 additions & 0 deletions components/PlayerDisplay.vue
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,12 @@ export default {
type: Boolean,
default: true,
},
// Presence from elsewhere (a search result's check); otherwise only
// friends and teammates are known.
online: {
type: Boolean,
default: undefined,
},
pingStatus: {
type: Boolean,
default: false,
Expand Down Expand Up @@ -518,6 +524,10 @@ export default {
return false;
}

if (this.online !== undefined) {
return this.online;
}

return useMatchmakingStore().onlinePlayerSteamIds.includes(
this.player.steam_id,
);
Expand Down
26 changes: 26 additions & 0 deletions components/PlayerSearch.vue
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ const { height: viewportHeight } = useVisualViewport();
: undefined
"
:player="player"
:online="onlineFor(player)"
:active="indexOfPlayer(player) === selectedIndex"
:reason="reasonFor(player)"
:tag="tagFor(player)"
Expand All @@ -112,6 +113,7 @@ const { height: viewportHeight } = useVisualViewport();
:key="`player-${player.steam_id}`"
:ref="index === selectedIndex ? setActiveRow : undefined"
:player="player"
:online="onlineFor(player)"
:active="index === selectedIndex"
:reason="reasonFor(player)"
:tag="tagFor(player)"
Expand Down Expand Up @@ -288,6 +290,7 @@ const { height: viewportHeight } = useVisualViewport();
: undefined
"
:player="player"
:online="onlineFor(player)"
:active="indexOfPlayer(player) === selectedIndex"
:reason="reasonFor(player)"
:tag="tagFor(player)"
Expand Down Expand Up @@ -319,6 +322,7 @@ const { height: viewportHeight } = useVisualViewport();
:key="`player-${player.steam_id}`"
:ref="index === selectedIndex ? setActiveRow : undefined"
:player="player"
:online="onlineFor(player)"
:active="index === selectedIndex"
:reason="reasonFor(player)"
:tag="tagFor(player)"
Expand Down Expand Up @@ -462,6 +466,8 @@ export default {
visibleCount: PAGE_SIZE,
// Guards against a slow early request overwriting a newer one.
searchToken: 0,
// Which loaded results the api says are online.
onlineResultIds: new Set<string>(),
observer: null as IntersectionObserver | null,
debouncedSearch: debounce((query: string) => {
this.searchPlayers(query);
Expand Down Expand Up @@ -789,6 +795,7 @@ export default {
this.page = 1;
this.visibleCount = PAGE_SIZE;
this.searchToken += 1;
this.onlineResultIds = new Set();
const token = this.searchToken;

if (this.onlineOnly) {
Expand All @@ -807,6 +814,7 @@ export default {
this.players = players;
this.totalFound = found;
this.selectedIndex = this.firstSelectableIndex();
void this.checkOnline(players, token);
} finally {
if (token === this.searchToken) {
this.searching = false;
Expand Down Expand Up @@ -843,6 +851,7 @@ export default {
this.players = (this.players ?? []).concat(
players.filter((p) => !seen.has(String(p.steam_id))),
);
void this.checkOnline(players, token);
} finally {
this.loadingMore = false;
// If the freshly appended page didn't push the sentinel out of the
Expand All @@ -851,6 +860,23 @@ export default {
this.$nextTick(() => this.ensureObserver());
}
},
async checkOnline(players: Player[], token: number) {
if (!players.length || !this.me) {
return;
}
const { online } = await useMatchmakingStore().checkOnline(
players.map((player) => String(player.steam_id)),
);
if (token !== this.searchToken) {
return;
}
this.onlineResultIds = new Set([...this.onlineResultIds, ...online]);
},
onlineFor(player: Player) {
return this.onlineResultIds.has(String(player.steam_id))
? true
: undefined;
},
ensureObserver() {
this.observer?.disconnect();
this.observer = null;
Expand Down
22 changes: 13 additions & 9 deletions components/PlayerSearchRow.vue
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
<script setup lang="ts">
import PlayerDisplay from "~/components/PlayerDisplay.vue";

defineProps<{
player: any;
active?: boolean;
// When set the row is rendered dimmed and non-selectable, and this sentence
// is shown underneath explaining why.
reason?: string;
tag?: string;
}>();
withDefaults(
defineProps<{
player: any;
active?: boolean;
// When set the row is rendered dimmed and non-selectable, and this
// sentence is shown underneath explaining why.
reason?: string;
tag?: string;
online?: boolean;
}>(),
{ online: undefined },
);

const emit = defineEmits<{ select: []; hover: [] }>();
</script>
Expand All @@ -28,7 +32,7 @@ const emit = defineEmits<{ select: []; hover: [] }>();
@mouseenter="emit('hover')"
>
<div class="flex items-center justify-between gap-2">
<PlayerDisplay :player="player" />
<PlayerDisplay :player="player" :online="online" />
<span
v-if="tag"
class="shrink-0 rounded-full border border-[hsl(var(--tac-amber)_/_0.4)] px-2 py-0.5 font-mono text-[0.6rem] uppercase tracking-[0.14em] text-[hsl(var(--tac-amber))]"
Expand Down
156 changes: 156 additions & 0 deletions components/hub/LanPlayersList.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
<script setup lang="ts">
import { computed, ref } from "vue";
import { Search } from "lucide-vue-next";
import { Input } from "~/components/ui/input";
import HubEmptyState from "~/components/hub/HubEmptyState.vue";
import FriendListItem from "~/components/matchmaking-lobby/FriendListItem.vue";
import { useMatchmakingStore } from "~/stores/MatchmakingStore";
import { useApplicationSettingsStore } from "~/stores/ApplicationSettings";

const matchmakingStore = useMatchmakingStore();
const authStore = useAuthStore();
const searchQuery = ref("");

const regionName = computed(() => {
const region = useApplicationSettingsStore().availableRegions.find(
(candidate: any) => candidate.value === matchmakingStore.lanRegion,
);
return region?.description || matchmakingStore.lanRegion;
});

// Like Others before it: accepted friends and incoming requests live in the
// Friends tab, and an outgoing request stays here so adding someone doesn't
// move them mid-action.
const listed = computed(() => {
const me = String(authStore.me?.steam_id);
const q = searchQuery.value.toLowerCase();

return (matchmakingStore.lanPlayers as any[]).filter((player) => {
if (String(player.steam_id) === me) {
return false;
}
const entry = (matchmakingStore.friends as any[])?.find(
(friend) => String(friend.steam_id) === String(player.steam_id),
);
if (
entry &&
(entry.status !== "Pending" || String(entry.invited_by_steam_id) !== me)
) {
return false;
}
return (
!q ||
player.name?.toLowerCase().includes(q) ||
String(player.steam_id).includes(searchQuery.value)
);
});
});

const left = computed(() => new Set(matchmakingStore.lanLeft as string[]));
const arrivals = computed(
() => new Set(matchmakingStore.lanArrivals as string[]),
);

const justArrived = computed(() =>
listed.value.filter(
(player) =>
arrivals.value.has(player.steam_id) && !left.value.has(player.steam_id),
),
);
const atLan = computed(() =>
listed.value.filter(
(player) =>
!arrivals.value.has(player.steam_id) || left.value.has(player.steam_id),
),
);
</script>

<template>
<div class="flex min-h-full flex-1 flex-col gap-3 p-2">
<div class="flex items-center gap-2 text-xs text-muted-foreground">
<span
data-testid="lan-region"
class="rounded border border-[hsl(var(--tac-amber)/0.4)] px-1.5 py-px font-mono text-[0.65rem] text-[hsl(var(--tac-amber))]"
>
{{ regionName }}
</span>
{{ $t("matchmaking.lan.everyone_here") }}
</div>
<div class="relative">
<Search class="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
v-model="searchQuery"
:placeholder="$t('matchmaking.lan.search_placeholder')"
class="pl-8"
/>
</div>

<section v-if="justArrived.length > 0" data-testid="lan-arrived">
<div class="lan-section-label">
{{ $t("matchmaking.lan.just_arrived") }}
<span class="ml-auto tabular-nums opacity-70">
{{ justArrived.length }}
</span>
</div>
<FriendListItem
v-for="player in justArrived"
:key="`arrived-${player.steam_id}`"
:player="player"
:online="true"
/>
</section>

<section v-if="atLan.length > 0" data-testid="lan-players">
<div class="lan-section-label">
{{ $t("matchmaking.lan.at_this_lan") }}
<span class="ml-auto tabular-nums opacity-70">
{{ atLan.length }}
</span>
</div>
<div
v-for="player in atLan"
:key="`lan-${player.steam_id}`"
:data-left="left.has(player.steam_id) || undefined"
>
<FriendListItem
:player="player"
:online="!left.has(player.steam_id)"
:muted="left.has(player.steam_id)"
/>
<p
v-if="left.has(player.steam_id)"
class="-mt-1 pb-1 pl-12 text-[0.65rem] text-muted-foreground"
>
{{ $t("matchmaking.lan.left") }}
</p>
</div>
</section>

<HubEmptyState
v-if="listed.length === 0"
:title="
searchQuery
? $t('layouts.hub.empty.no_match', { query: searchQuery })
: $t('matchmaking.lan.empty_title')
"
:description="
searchQuery ? null : $t('matchmaking.lan.empty_description')
"
/>
</div>
</template>

<style scoped>
.lan-section-label {
display: flex;
align-items: center;
gap: 0.4rem;
margin-bottom: 0.4rem;
font-family: var(--font-mono, ui-monospace, monospace);
font-size: 0.6rem;
font-weight: 700;
letter-spacing: 0.18em;
text-transform: uppercase;
color: hsl(var(--muted-foreground));
}
</style>
Loading
Loading